mirror of
https://github.com/rickbarrette/redmine_qbo.git
synced 2025-11-08 08:54:23 -05:00
Compare commits
592 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 32b750b545 | |||
| 5fd3141746 | |||
| 2c38361234 | |||
| 81b7b1492d | |||
| 57ef1ac5a1 | |||
| 6597c5a13c | |||
| 8af97072fb | |||
| 48b6df0cef | |||
| 853b7ad804 | |||
| 6a74baff5e | |||
| 8b21b0ff75 | |||
| d22a6303cd | |||
| 807d6643f4 | |||
| c725c2774c | |||
| ae0abae333 | |||
| fed2282212 | |||
| 545960e676 | |||
| 66781f0625 | |||
| 504b9b93e4 | |||
| b71bba473c | |||
| aef3c453c4 | |||
| 6de3ed94dc | |||
| daada08ea7 | |||
| fa37c98500 | |||
| 0ee59704b3 | |||
| a22cbb9520 | |||
| c0d3f64d82 | |||
| 66d2bf4aa4 | |||
| 2b90c953ba | |||
| 0d5e5d679e | |||
| 4da891cc07 | |||
| d3475a9b53 | |||
| eb583f78ec | |||
| 47bebf0a1a | |||
| f9bd149f21 | |||
| 5371b0f193 | |||
| 95497e5514 | |||
| 1a74abe76c | |||
| 7f11d3cdc4 | |||
| da01d79325 | |||
| 82807cfede | |||
| a268d10819 | |||
| ba4bdd9ecb | |||
| 0a72e05e03 | |||
| 8d143ff06d | |||
| 3ddb585a58 | |||
| 1606ceb743 | |||
| 97578f8380 | |||
| 32beae70ef | |||
| 13fbc9e14f | |||
| c57a45c85e | |||
| 3711a9ca43 | |||
| d0c1693f38 | |||
| 36534ee129 | |||
| 188c460054 | |||
| d9e3cb096b | |||
| b57b19493f | |||
| 8c569db541 | |||
| 997257f42d | |||
| f9f1af17bc | |||
| f1f44d0048 | |||
| 7e8511090d | |||
| 4ca6a3138b | |||
| 21e1132e0e | |||
| dc15424014 | |||
| f90cdcd86b | |||
| 5e53c18098 | |||
| 52d13ea7bc | |||
| 5e24c5084e | |||
| abdb61cc41 | |||
| 03556cc670 | |||
| 7f6cd99aba | |||
| ba513fb950 | |||
| 837ddd722c | |||
| f2b0cd3748 | |||
| f2a8878af4 | |||
| 13fccec54b | |||
| eca2b986a9 | |||
| a06599b7f9 | |||
| 7fda4dc577 | |||
| 9e47152e12 | |||
| 83d21da41a | |||
| a692f03bfa | |||
| 994cdf908f | |||
| b022d17fc0 | |||
| 644899c0b5 | |||
| be3a3b920d | |||
| d546eb026f | |||
| fdc59feb13 | |||
| 186b726a7b | |||
|
|
fa362bad55 | ||
|
|
fcf55bb504 | ||
|
|
2185667665 | ||
|
|
772483817e | ||
|
|
178ddd32c7 | ||
| 08e047c90e | |||
| b3c3314385 | |||
| 9fd5e01bb4 | |||
| cd62f65fcd | |||
| fb40833abd | |||
| 6aae155933 | |||
| f9e0ae8fef | |||
| 489e335ca4 | |||
| 874d0b4db9 | |||
| 49e8f70b46 | |||
| 77ea20171e | |||
| 11d4034c37 | |||
| 64369470de | |||
| 7b483f3290 | |||
| 32bec79c28 | |||
| dfd9622ab7 | |||
| 334d3c930b | |||
| 8cf2f370bf | |||
| 3965bed6c4 | |||
| 52396eb384 | |||
| 9cfab7bea1 | |||
| c8ef3bbd4e | |||
| 39e7d3c062 | |||
| 6fa96e11df | |||
| ecde64193a | |||
| f701af9a4d | |||
| 6d99702a11 | |||
| 138f8f2c2f | |||
| 61ddf7378d | |||
| f5b72f30be | |||
| 1863b33955 | |||
| a4573fce1c | |||
| 0461801ee0 | |||
| 4c2eaac013 | |||
| 7ca56ccd2e | |||
| 915a59afa4 | |||
| ac61950d48 | |||
| b257fef563 | |||
| 504c27c197 | |||
| a7a5e2c731 | |||
| 21d72dcc33 | |||
| da7ba40e61 | |||
| b4d6fc55ea | |||
| 515b8feff7 | |||
| 8bc05db033 | |||
| 34cd6b08dc | |||
| 41195dc095 | |||
| 33a83c8f76 | |||
| 4f613d3fe1 | |||
| 1c7cdec600 | |||
| 7ae60c0e62 | |||
| 5dd04925e0 | |||
| 92eedbd4d3 | |||
| 5545d72adf | |||
| 226d44cd28 | |||
| b7152d6124 | |||
| f3e9b58c87 | |||
| 5209315236 | |||
| f38a9e1ff0 | |||
| 80fb296e24 | |||
| 9dda339a32 | |||
| 1098accc8a | |||
| 12826cf436 | |||
| f9f77fdcb1 | |||
| 0bc935d3dd | |||
| fc40e4a6fe | |||
| 99b658a03d | |||
| dacde1e050 | |||
| 365219eddb | |||
| bc4dbbadbb | |||
| c06d2300f2 | |||
| 5e34587a53 | |||
| eb39b297f9 | |||
| 798a7c9933 | |||
| 9dee336e76 | |||
| 9ba43d63b9 | |||
| 8126671df9 | |||
| 3f0ccd79f3 | |||
| e61023acd5 | |||
| 35c2e7a951 | |||
| 81e8a9594f | |||
| 63415f8e58 | |||
| ed9b1ea7b9 | |||
| 6026f9cdfc | |||
| 1924156a8c | |||
| 3927b2b007 | |||
| 7a2e984df7 | |||
| 2c9559104a | |||
| 07e342845a | |||
| dbab5bfbca | |||
| cc70c95115 | |||
| 03bcff2b9a | |||
| 9c4ed3d9e1 | |||
| 8156657eb2 | |||
| c2ffedc8de | |||
| 3600c3e80a | |||
| 2970bd092c | |||
| a2cac188bb | |||
| e542f098a8 | |||
| 04a1670ac2 | |||
| c189bc5dca | |||
| 4b7cf407e8 | |||
| 332deed21e | |||
| 41313029cd | |||
| bbc3b138cf | |||
| e4d5770bdc | |||
| 53a0a47dd6 | |||
| 48edc85e2c | |||
| c685aaa245 | |||
| 2a79389b18 | |||
| c3e4d0dbc2 | |||
| 89123fed31 | |||
| 571811ace6 | |||
| cb24967713 | |||
| 449a59188c | |||
| 907448ce3e | |||
| 4f08af3987 | |||
| 00285e1f24 | |||
| 5b56d7a878 | |||
| 5c0d1def9f | |||
| fc3e252fff | |||
| d605f617e4 | |||
| eb390e09d8 | |||
| bde29ef9d0 | |||
| 5ffc9ed01c | |||
| c992370962 | |||
| b3acf9f29d | |||
| dca3735ce1 | |||
| c7a5c1147f | |||
| 8940e72091 | |||
| 1ed7c6fe63 | |||
| a197dcdefc | |||
| e00f73a48d | |||
| 1c977a6687 | |||
| b3e93bb465 | |||
| 628c798238 | |||
| b34bd9dd7c | |||
| 4d40093fe9 | |||
| e573da2c11 | |||
| f47316efbe | |||
| f57c3c3df0 | |||
| 9b91e4fd63 | |||
| a264e707a8 | |||
| a75a784e8d | |||
| 1e04b6ae9f | |||
| 6dfbfccced | |||
| 27288c2eb2 | |||
| 53a1be9761 | |||
| d1c6492ea3 | |||
| 0f72d88c71 | |||
| 9edfcecdaa | |||
| 5303b3dfef | |||
| 1213c2e57a | |||
| 49ca69aa78 | |||
| 586f7c8fb9 | |||
| 7a68fcfa92 | |||
| 357e5d4490 | |||
| af25326c23 | |||
| e1db312982 | |||
| 59a418727e | |||
| c3d9833acb | |||
| 59ebeb48ce | |||
| 7bd23e993e | |||
| 9b15f3f4f6 | |||
| f087d3c6c0 | |||
| 806b4719fe | |||
| 126f4abe0a | |||
| 5ec76737b3 | |||
| 1d7bcc24fe | |||
| 76a6fce406 | |||
| 3d44bcb04d | |||
| fd8b5c280c | |||
| ef6f104d5f | |||
| 92538a58e3 | |||
| 44bc2f47f1 | |||
| 3c9316340f | |||
| 3ef9236388 | |||
| dfdde631f9 | |||
| 372a6a1b6a | |||
| 5a4996abac | |||
| 416df8d3f1 | |||
| 0aa7fe8e73 | |||
| f14e82a01b | |||
| 5a10065bb0 | |||
| fb801e9260 | |||
| 0fa31f815e | |||
| 76bd0d4e08 | |||
| b31c3ad550 | |||
| af7c1b0130 | |||
| 224b0b4238 | |||
| e6fee6bd97 | |||
| 731b811cfe | |||
| 63d969c844 | |||
| 1138b0d5c9 | |||
| 758810135d | |||
| 6eff61b19d | |||
| b3bc17f327 | |||
| 67d4ac0ebf | |||
| 11d3a2d0bf | |||
| 1f76333af7 | |||
| 816daeb429 | |||
| b1bc19fb7a | |||
| 578258f9e2 | |||
| 41fe8f6a5d | |||
| fee0548899 | |||
| 5ddb45ba24 | |||
| 98c965c607 | |||
| 6e1d23af4e | |||
| 8da45bd348 | |||
| 620c4b395e | |||
| d75208a75a | |||
| 4f08825fb1 | |||
| 865470fc11 | |||
| d827936c85 | |||
| fcc614ff54 | |||
| b7abe2610e | |||
| d916464423 | |||
| 32164157c2 | |||
| ce4b957c8e | |||
| 3735629073 | |||
| d41d618be5 | |||
| d97f3cb2a3 | |||
| dcf31116b4 | |||
| 219141eeee | |||
| 765b5b6024 | |||
| 4efca93d03 | |||
| e2a4908420 | |||
| 183b8d17e6 | |||
| ed2b84c697 | |||
| 59410e6d77 | |||
| a134d1b601 | |||
| 56161f12d0 | |||
| 146dbb137c | |||
| 4f23439dac | |||
| 8b33aa6f6a | |||
| 4f72a8e5ad | |||
| fae4782ef0 | |||
| 37ea01de8c | |||
| 2c53155207 | |||
| dbe585ca2a | |||
| 6434092306 | |||
| 8720176b57 | |||
| 5bdf313fa5 | |||
| 4527e74d29 | |||
| 8e6e543c5b | |||
| dbbd4a2593 | |||
| 6b55f92454 | |||
| fba9645932 | |||
| 0cc867b410 | |||
| 40f738d976 | |||
| 82ecaae156 | |||
| 8d4ac896fa | |||
| 7c6246a539 | |||
| ce88cdd258 | |||
| f179b04af1 | |||
| 0070264d51 | |||
| 22db89a6d9 | |||
| 78dfad9875 | |||
| 6a55138f7c | |||
| f95ee10290 | |||
| adb864c9ca | |||
| f3f92e48e0 | |||
| 87a9d978c2 | |||
| 9981b9ef70 | |||
| 94f10dc9cd | |||
| e3fdc070df | |||
| 4cae63d02b | |||
| d856ceeec7 | |||
| d461570b14 | |||
| 924e0d7bc9 | |||
| 6b3280edaf | |||
| 722d66f130 | |||
| 16083a6f30 | |||
| 1b6fe073dc | |||
| 9f6103ad89 | |||
| 8a67cdf37c | |||
| 9b444d638b | |||
| abab81158f | |||
| 26c0716d35 | |||
| 7f7c724ef1 | |||
| f083e8257a | |||
| 4cb588e992 | |||
| 245d2b49a2 | |||
| e888bd0d38 | |||
| 6cfd56ed01 | |||
| 647af6f87a | |||
| 0e2f9b1031 | |||
| 86ee8908b3 | |||
| a0618b51ba | |||
| 272369ba4c | |||
| 6319c24b5c | |||
| a3180a318c | |||
| aeb890cbed | |||
| 4a94ca1d17 | |||
| 63d845ed97 | |||
| cb67cab974 | |||
| df94564d9b | |||
| 540e008f68 | |||
| c85d3ba8d5 | |||
| 1df335ed16 | |||
| 7ca5076477 | |||
| 6605946e62 | |||
| ba45c776ae | |||
| dcf17052b6 | |||
| 8b9acccb8a | |||
| 2afed176f0 | |||
| 577788110e | |||
| d251ea066f | |||
| 609e65b7cd | |||
| 6c2dd29a57 | |||
| 98896ac0a6 | |||
| 19ba3abade | |||
| 3347490b82 | |||
| d1457b09be | |||
| 1b71439f19 | |||
| 55c09d7e9d | |||
| 8429c29c30 | |||
| 10f8a7e124 | |||
| e8763ea923 | |||
| e5601030b1 | |||
| ad8d15203e | |||
| 3b4e55727c | |||
| 5dc2921d40 | |||
| 0c4ef8abe9 | |||
| 8165523acf | |||
| 7d1e9bb838 | |||
| 0d9140958f | |||
| 16ca8177e9 | |||
| a0e9061a8f | |||
| a56c01fe6d | |||
| 1cb9639f03 | |||
| 7af89db442 | |||
| fae815fd7f | |||
| 1b533d6dd8 | |||
| bc38361348 | |||
| a0a365c10e | |||
| 162c76471b | |||
| 328a50be47 | |||
| 7cc84277c6 | |||
| fbac6b6d77 | |||
| 33b5ac8c87 | |||
| 74f179d64b | |||
| 3cef188ff3 | |||
| 1b767f78d2 | |||
| f380969082 | |||
| df6acde327 | |||
| 0318ffaa10 | |||
| 51c1b38197 | |||
| d96bd1a3f4 | |||
| b6e43b5837 | |||
| 62fa98a656 | |||
| bb5a080f25 | |||
| 2afa9e4166 | |||
| b489a2771f | |||
| 0495ac1bc5 | |||
| e3b49358bb | |||
| 08b365e69e | |||
| 5d4c49c85d | |||
| 5bc9ca34a4 | |||
| 630a1d144b | |||
| 491684f7df | |||
| 9a28247b7f | |||
| 5a91e21d45 | |||
| f6f1ca4c04 | |||
| 8daa10888f | |||
| 82449642d3 | |||
| 06ad2d6971 | |||
| 4c4ca67be8 | |||
| b994f7c142 | |||
| 97b483031d | |||
| c624c20354 | |||
| 695e3bd24c | |||
| ce4883cd4c | |||
| 49e19cb73f | |||
| 31bb242a61 | |||
| 2e533e8798 | |||
| e70d0c8d17 | |||
| d96ecd2b66 | |||
| a588ac19a6 | |||
| 2bc8ec4f56 | |||
| 578a5a1228 | |||
| 416595ffea | |||
| d3be59fbc5 | |||
| 3c3b4da313 | |||
| 501834419b | |||
| 75b25a9e44 | |||
| 15912b2197 | |||
| b093f6136e | |||
| 33db0a53ba | |||
| 7237d2e643 | |||
| 77b1c1dbef | |||
| f3090bd1a4 | |||
| 89a131018c | |||
| de17fb80d1 | |||
| a5f1d15156 | |||
| d3463ce41b | |||
| 4503150b02 | |||
| 36cd00822e | |||
| d285344a61 | |||
| 8418dfc0b5 | |||
| eb039368bb | |||
| 0dea5917a7 | |||
| a8ccde6c81 | |||
| 787ae1b8df | |||
| 276c89d4ac | |||
| 9a395ee25c | |||
| 475c86eabe | |||
| 259737a488 | |||
| 362cb77381 | |||
| 8cfab17136 | |||
| f0018ab87d | |||
| 8f87eb3e60 | |||
| 2b093903b3 | |||
| 05017dcc4f | |||
| 0e9b5fa17a | |||
| dd335aff71 | |||
| 0f61bf54ce | |||
| 14cb22d743 | |||
| 702ab5013e | |||
| 235e2c6e7b | |||
| 2e89a60d63 | |||
| 3d5ef2cd8a | |||
| de8eff9bd2 | |||
| a9561d1694 | |||
| aa33de00d2 | |||
| ffc589fe80 | |||
| 229e4e8d39 | |||
| d6dda2cdd6 | |||
| b8a101fddb | |||
| c8a875b301 | |||
| df8e3a7465 | |||
| d91a6e3939 | |||
| 48a2d683dd | |||
| 44bf42c548 | |||
| d34e6cb0fd | |||
| d8e7356ca3 | |||
| 60e6dbaa6f | |||
| 47e5a7d0e4 | |||
| 9fa2165907 | |||
| 7385d7018c | |||
| 6124c1b307 | |||
| b73535c6da | |||
| 1581023656 | |||
| 0d21e2967d | |||
| 0dc7d83fbe | |||
| cd18067384 | |||
| 6c99f7095c | |||
| eeaafce427 | |||
| b7cb27b5da | |||
| 3e6286da7c | |||
| 30ceea7fd5 | |||
| de9e973fd9 | |||
| 49a3bd5790 | |||
| f1745930b1 | |||
| d9beda8171 | |||
| 65f343fb74 | |||
| 892bd65fac | |||
| 0251191844 | |||
| 65f6f52252 | |||
| 4d94308bcc | |||
| 7dcd8b24d2 | |||
| 11da8e7a43 | |||
| 56c895388d | |||
| 8ec9567f15 | |||
| be3dd0d131 | |||
| 92f51d9884 | |||
| c4904a0ac2 | |||
| 0d87e5fb21 | |||
| d38e3e1702 | |||
| fec59a7495 | |||
| a3b5ad0cb0 | |||
| bf21451819 | |||
| c6d3d9673b | |||
| 3f5334a92d | |||
| bde7b83752 | |||
| c788e5724a | |||
| 295cd12f9d | |||
| 4a432481d9 | |||
| 4a37d83694 | |||
| 15a2a16379 | |||
| 18fc7a6c8c | |||
| 7aba8cdce3 | |||
| 382e6675f1 | |||
| 116d6896f4 | |||
| c9ced52112 | |||
| 01b4bb4e53 | |||
| a266da2cd7 | |||
| 578e7ba807 | |||
| b923e15d46 | |||
| 1310d1e63e | |||
| a8e1e8429c | |||
| 1b54b40f6c | |||
| 6d7530922d | |||
| 23698986b1 | |||
| 1b4c377940 | |||
| d33c0c9aa5 | |||
| 09d8c0024f |
9
Gemfile
9
Gemfile
@@ -6,4 +6,11 @@ gem 'oauth-plugin'
|
||||
gem 'oauth'
|
||||
gem 'roxml'
|
||||
gem 'edmunds_vin'
|
||||
gem 'will_paginate', '~> 3.1.0'
|
||||
gem 'will_paginate'
|
||||
gem 'rails-jquery-autocomplete'
|
||||
gem 'jquery-rails', '~> 3.1.4'
|
||||
gem 'jquery-ui-rails'
|
||||
|
||||
group :assets do
|
||||
gem 'coffee-rails'
|
||||
end
|
||||
|
||||
55
README.md
55
README.md
@@ -1,20 +1,31 @@
|
||||
#Redmine Quickbooks Online
|
||||
|
||||
A simple plugin for Redmine to connect to Quickbooks Online
|
||||
A plugin for Redmine to connect to Quickbooks Online
|
||||
|
||||
The goal of this project is to allow redmine to connect with Quickbooks Online to create time activity entries for completed work when an issue is closed.
|
||||
The goal of this project is to allow Redmine to connect with Quickbooks Online to create `Time Activity Entries` for completed work when an Issue is closed.
|
||||
|
||||
`Note: This project is under heavy development. Currently the initial functionality goal has been meet, however I am still working on adding other features. Tags should be stable`
|
||||
`Note: Although the core functionality is complete, this project is still under heavy development. I am still working on refining everthing and adding other features. Tags should be stable`
|
||||
|
||||
####How it works
|
||||
* Issues can be assigned to a QBO Customer and QBO Service Item via drop down in issues form
|
||||
- The `QBO Employee` for the issue is assigned via the assigned redmine user
|
||||
- IF an `Issue` has been assined a `QBO Customer`, `QBO Service Item` & `QBO Employee` when an `Issue` is closed the following will happen:
|
||||
- A new `QBO Time Activity` agaist the `QBO Customer` will be created using the total spent hours logged agaist an `Issue`.
|
||||
- The rate will be the set via the `QBO Service Item` price
|
||||
* `Issues` with the Tracker `Quote` will generate an estimate based on the estimated hours and `QBO Service Item` cost.
|
||||
- Needs to have a `QBO Customer` & `QBO Service Item` Assiged
|
||||
* Users will be assigned a `QBO Employee` via a drop down in the user admistration page.
|
||||
`Note: I am currently using this in a live production enviroment with no issues`
|
||||
|
||||
####Features
|
||||
* Issues can be assigned to a `Customer` via drop down in the edit Issue form
|
||||
* The `Employee` for the Issue is assigned via the assigned Redmine User
|
||||
- This is set via a drop down in the user admistration page.
|
||||
* IF an `Issue` has been assined a `Customer` when an Issue is closed the following will happen:
|
||||
- A new `Time Activity` will be billed agaist the `Customer` assinged to the issue for each Redmine Time Entery.
|
||||
+ Time Entries will be totalled up by Activity name. This will allow billing for diffrent activities without having to create seperate Issues.
|
||||
+ The Time Activity names are used to lookup `Items` in Quickbooks.
|
||||
+ IF there isn'tany Items that match the Activity name it will be skipped, and will not be billed to the `Customer`
|
||||
- Labor Rates are set by the `Item` in Quickbooks
|
||||
* `Payments` Can be created via the Redmine application menu
|
||||
* `Customers` Can be created via the Redmine application menu
|
||||
- `Customers` can be searched
|
||||
- Basic information for the `Customer` can be viewed/edit via the Customer page
|
||||
* Webhook Support
|
||||
- `Invoices` are automaticly attached to an Issue if a line item has a hashtag number in a `Line Item`
|
||||
+ `Invoice` Custom Fields are matched Issue Custom Fileds and are automaticly updated in Quickbooks. For example, this is usefull for extracting the Mileage In / Out from the Issue and updating the Invoice with the information.
|
||||
- `Customers` are automaticly updated in local database
|
||||
|
||||
##Prerequisites
|
||||
|
||||
@@ -35,28 +46,32 @@ The goal of this project is to allow redmine to connect with Quickbooks Online t
|
||||
|
||||
3. Navigate to the plugin configuration page and suppy your own OAuth key & secret.
|
||||
|
||||

|
||||
|
||||
4. After saving your key & secret, you need to click on the Authenticate link on the plugin configuration page to authenticate with QBO.
|
||||
|
||||
5. Assign an Employee to each of your users via the User Administration Page
|
||||
|
||||

|
||||
## Automatic Deploy
|
||||
|
||||
If you want the redmine server to be automaticly restarted after a git pull event add this hook to your git hook directory
|
||||
https://gist.github.com/rickbarrette/3c999c7f37e321f9c60380de99e494f5
|
||||
|
||||
## Usage
|
||||
|
||||
To enable automatic `QBO Time Activity` entries for an `Issue` , you need only to assign a `QBO Customer` and `QBO Item` to an `Issue` via drop downs in the creation/update form.
|
||||
|
||||

|
||||
To enable automatic `Time Activity` entries for an Issue , you need only to assign a `Customer` to an Issue via drop downs in the issue creation/update form.
|
||||
|
||||
Note: After the inital synchronization, this plugin will recieve push notifications via Intuit's webhook service.
|
||||
|
||||
## TODO
|
||||
* Abiltiy to add line items to a ticket in a dynamic table so they can be added to the invoice upon closing of the issue
|
||||
* Customer ~~Creation~~, ~~Update~~, Deletion
|
||||
* Customer Deletion
|
||||
* Email Customer updates, provding a link that would: bypass the login page, go directly to the issue directing them to, and allow them to view only that issue.
|
||||
* Add a rake file to create required Trackers or statuses required
|
||||
* Add Setting for Sandbox Mode
|
||||
* Refactor Models prefixed with Qbo...
|
||||
* Seperate Vehicles into a seperate plugin
|
||||
* Make HTML Pretty
|
||||
* Intergrate Customer Search into Redmine Search
|
||||
* Fix Issue sort by Customer
|
||||
* MORE Stuff...
|
||||
|
||||
##License
|
||||
|
||||
|
||||
@@ -13,11 +13,31 @@ class CustomersController < ApplicationController
|
||||
unloadable
|
||||
|
||||
include AuthHelper
|
||||
helper :issues
|
||||
helper :journals
|
||||
helper :projects
|
||||
helper :custom_fields
|
||||
helper :issue_relations
|
||||
helper :watchers
|
||||
helper :attachments
|
||||
helper :queries
|
||||
include QueriesHelper
|
||||
helper :repositories
|
||||
helper :sort
|
||||
include SortHelper
|
||||
helper :timelog
|
||||
|
||||
before_filter :require_user
|
||||
before_filter :require_user, :except => :view
|
||||
skip_before_filter :verify_authenticity_token, :check_if_login_required, :only => [:view]
|
||||
|
||||
default_search_scope :names
|
||||
|
||||
autocomplete :customer, :name, :full => false, :extra_data => [:id]
|
||||
|
||||
def filter_vehicles_by_customer
|
||||
@filtered_vehicles = Vehicle.all.where(customer_id: params[:selected_customer])
|
||||
end
|
||||
|
||||
# display a list of all customers
|
||||
def index
|
||||
if params[:search]
|
||||
@@ -89,6 +109,39 @@ class CustomersController < ApplicationController
|
||||
end
|
||||
end
|
||||
|
||||
# Customer view for an issue
|
||||
def view
|
||||
|
||||
User.current = User.find_by lastname: 'Anonymous'
|
||||
|
||||
@token = CustomerToken.where("token = ? and expires_at > ?", params[:token], Time.now)
|
||||
@token = @token.first
|
||||
if @token
|
||||
session[:token] = @token.token
|
||||
@issue = Issue.find @token.issue_id
|
||||
@journals = @issue.journals.
|
||||
preload(:details).
|
||||
preload(:user => :email_address).
|
||||
reorder(:created_on, :id).to_a
|
||||
@journals.each_with_index {|j,i| j.indice = i+1}
|
||||
@journals.reject!(&:private_notes?) unless User.current.allowed_to?(:view_private_notes, @issue.project)
|
||||
Journal.preload_journals_details_custom_fields(@journals)
|
||||
@journals.select! {|journal| journal.notes? || journal.visible_details.any?}
|
||||
@journals.reverse! if User.current.wants_comments_in_reverse_order?
|
||||
|
||||
@changesets = @issue.changesets.visible.preload(:repository, :user).to_a
|
||||
@changesets.reverse! if User.current.wants_comments_in_reverse_order?
|
||||
|
||||
@relations = @issue.relations.select {|r| r.other_issue(@issue) && r.other_issue(@issue).visible? }
|
||||
@allowed_statuses = @issue.new_statuses_allowed_to(User.current)
|
||||
@priorities = IssuePriority.active
|
||||
@time_entry = TimeEntry.new(:issue => @issue, :project => @issue.project)
|
||||
@relation = IssueRelation.new
|
||||
else
|
||||
render_403
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def only_one_non_zero?( array )
|
||||
|
||||
@@ -12,13 +12,14 @@ class InvoiceController < ApplicationController
|
||||
|
||||
include AuthHelper
|
||||
|
||||
before_filter :require_user
|
||||
before_filter :require_user, :unless => proc {|c| session[:token].nil? }
|
||||
skip_before_filter :verify_authenticity_token, :check_if_login_required, :unless => proc {|c| session[:token].nil? }
|
||||
|
||||
#
|
||||
# Downloads and forwards the invoice pdf
|
||||
#
|
||||
def show
|
||||
base = QboInvoice.get_base.service
|
||||
base = QboInvoice.get_base
|
||||
invoice = base.fetch_by_id(params[:id])
|
||||
@pdf = base.pdf(invoice)
|
||||
send_data @pdf, filename: "invoice #{invoice.doc_number}.pdf", :disposition => 'inline', :type => "application/pdf"
|
||||
|
||||
51
app/controllers/payments_controller.rb
Normal file
51
app/controllers/payments_controller.rb
Normal file
@@ -0,0 +1,51 @@
|
||||
#The MIT License (MIT)
|
||||
#
|
||||
#Copyright (c) 2016 rick barrette
|
||||
#
|
||||
#Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
#
|
||||
#The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
#
|
||||
#THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
class PaymentsController < ApplicationController
|
||||
unloadable
|
||||
|
||||
include AuthHelper
|
||||
|
||||
before_filter :require_user
|
||||
|
||||
def new
|
||||
@payment = Payment.new
|
||||
|
||||
@customers = Customer.all.sort_by &:name
|
||||
|
||||
@accounts = Qbo.get_base(:account).service.query("SELECT Id, Name FROM Account WHERE AccountType = 'Bank' Order By Name")
|
||||
|
||||
@payment_methods = Qbo.get_base(:payment_method).service.all
|
||||
end
|
||||
|
||||
def create
|
||||
@payment = Payment.new(params[:payment])
|
||||
if @payment.save
|
||||
flash[:notice] = "Payment Saved"
|
||||
redirect_to Customer.find_by_id(@payment.customer_id)
|
||||
else
|
||||
flash[:error] = @payment.errors.full_messages.to_sentence
|
||||
redirect_to new_customer_path
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def only_one_non_zero?( array )
|
||||
found_non_zero = false
|
||||
array.each do |val|
|
||||
if val!=0
|
||||
return false if found_non_zero
|
||||
found_non_zero = true
|
||||
end
|
||||
end
|
||||
found_non_zero
|
||||
end
|
||||
|
||||
end
|
||||
@@ -16,7 +16,7 @@ class QboController < ApplicationController
|
||||
include AuthHelper
|
||||
|
||||
before_filter :require_user, :except => :qbo_webhook
|
||||
skip_before_filter :verify_authenticity_token, :check_if_login_required
|
||||
skip_before_filter :verify_authenticity_token, :check_if_login_required, :only => [:qbo_webhook]
|
||||
|
||||
#
|
||||
# Called when the QBO Top Menu us shown
|
||||
@@ -57,12 +57,23 @@ class QboController < ApplicationController
|
||||
qbo.reconnect_token_at = 5.months.from_now.utc
|
||||
qbo.company_id = params['realmId']
|
||||
if qbo.save!
|
||||
redirect_to qbo_path, :flash => { :notice => "Successfully connected to Quickbooks" }
|
||||
redirect_to qbo_sync_path, :flash => { :notice => "Successfully connected to Quickbooks" }
|
||||
else
|
||||
redirect_to plugin_settings_path(:redmine_qbo), :flash => { :error => "Error" }
|
||||
end
|
||||
end
|
||||
|
||||
# Manual Billing
|
||||
def bill
|
||||
i = Issue.find_by_id params[:id]
|
||||
if i.customer
|
||||
i.bill_time
|
||||
redirect_to i, :flash => { :notice => "Successfully Billed #{i.customer.name}" }
|
||||
else
|
||||
redirect_to i, :flash => { :error => "Cannot bill without a customer assigned" }
|
||||
end
|
||||
end
|
||||
|
||||
# Quickbooks Webhook Callback
|
||||
def qbo_webhook
|
||||
|
||||
@@ -118,15 +129,19 @@ class QboController < ApplicationController
|
||||
# Synchronizes the QboCustomer table with QBO
|
||||
#
|
||||
def sync
|
||||
if Qbo.exists?
|
||||
Customer.sync
|
||||
QboItem.sync
|
||||
QboEmployee.sync
|
||||
QboEstimate.sync
|
||||
QboInvoice.sync
|
||||
|
||||
# Record the last sync time
|
||||
Qbo.update_time_stamp
|
||||
# Update info in background
|
||||
Thread.new do
|
||||
if Qbo.exists?
|
||||
Customer.sync
|
||||
QboItem.sync
|
||||
QboEmployee.sync
|
||||
QboEstimate.sync
|
||||
QboInvoice.sync
|
||||
|
||||
# Record the last sync time
|
||||
Qbo.update_time_stamp
|
||||
end
|
||||
ActiveRecord::Base.connection.close
|
||||
end
|
||||
|
||||
redirect_to qbo_path(:redmine_qbo), :flash => { :notice => "Successfully synced to Quickbooks" }
|
||||
|
||||
@@ -18,6 +18,14 @@ class VehiclesController < ApplicationController
|
||||
|
||||
# display a list of all vehicles
|
||||
def index
|
||||
if params[:customer_id]
|
||||
begin
|
||||
@vehicles = Customer.find_by_id(params[:customer_id]).vehicles.paginate(:page => params[:page])
|
||||
rescue ActiveRecord::RecordNotFound
|
||||
render_404
|
||||
end
|
||||
end
|
||||
|
||||
if params[:search]
|
||||
@vehicles = Vehicle.search(params[:search]).paginate(:page => params[:page])
|
||||
if only_one_non_zero?(@vehicles)
|
||||
@@ -30,6 +38,7 @@ class VehiclesController < ApplicationController
|
||||
def new
|
||||
@vehicle = Vehicle.new
|
||||
@customers = Customer.all.order(:name)
|
||||
@customer = params[:customer_id] if params[:customer_id]
|
||||
end
|
||||
|
||||
# create a new vehicle
|
||||
@@ -92,6 +101,15 @@ class VehiclesController < ApplicationController
|
||||
end
|
||||
end
|
||||
|
||||
# returns a dynamic list of vehicles owned by a customer
|
||||
def update_vehicles
|
||||
@vehicles = Customer.find_by(customer_id: params[:customer_id].to_i).vehicles
|
||||
respond_to do |format|
|
||||
format.html { render(:text => "not implemented") }
|
||||
format.js
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def only_one_non_zero?( array )
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
module AuthHelper
|
||||
|
||||
def require_user
|
||||
return unless session[:token].nil?
|
||||
if !User.current.logged?
|
||||
render :file => "public/401.html.erb", :status => :unauthorized, :layout =>true
|
||||
end
|
||||
|
||||
@@ -142,7 +142,18 @@ class Customer < ActiveRecord::Base
|
||||
|
||||
# Searchs the database for a customer by name
|
||||
def self.search(search)
|
||||
where("name LIKE ?", "%#{search}%").order(:name)
|
||||
customers = where("name LIKE ?", "%#{search}%")
|
||||
|
||||
#if customers.empty?
|
||||
# service = Qbo.get_base(:customer).service
|
||||
# results = service.query("Select Id From Customer Where PrimaryPhone LIKE '%#{search}%' AND Mobile LIKE '%#{search}%'")
|
||||
|
||||
# results.each do |customer|
|
||||
# customers << Customer.find_by_id(customer.id)
|
||||
# end
|
||||
#end
|
||||
|
||||
return customers.order(:name)
|
||||
end
|
||||
|
||||
# proforms a bruteforce sync operation
|
||||
|
||||
23
app/models/customer_token.rb
Normal file
23
app/models/customer_token.rb
Normal file
@@ -0,0 +1,23 @@
|
||||
#The MIT License (MIT)
|
||||
#
|
||||
#Copyright (c) 2016 rick barrette
|
||||
#
|
||||
#Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
#
|
||||
#The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
#
|
||||
#THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
class CustomerToken < ActiveRecord::Base
|
||||
unloadable
|
||||
has_many :issues
|
||||
attr_accessible :token, :expires_at, :issue_id
|
||||
validates_presence_of :expires_at, :issue_id
|
||||
before_create :generate_token
|
||||
|
||||
OAUTH_CONSUMER_SECRET = Setting.plugin_redmine_qbo['settingsOAuthConsumerSecret'] || 'CONFIGURE_QBO__' + SecureRandom.uuid
|
||||
|
||||
def generate_token
|
||||
self.token = SecureRandom.base64(15).tr('+/=lIO0', OAUTH_CONSUMER_SECRET)
|
||||
end
|
||||
end
|
||||
37
app/models/payment.rb
Normal file
37
app/models/payment.rb
Normal file
@@ -0,0 +1,37 @@
|
||||
#The MIT License (MIT)
|
||||
#
|
||||
#Copyright (c) 2016 rick barrette
|
||||
#
|
||||
#Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
#
|
||||
#The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
#
|
||||
#THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
class Payment
|
||||
unloadable
|
||||
|
||||
include ActiveModel::Model
|
||||
|
||||
attr_accessor :errors, :customer_id, :account_id, :payment_method_id, :total_amount
|
||||
validates_presence_of :customer_id, :account_id, :payment_method_id, :total_amount
|
||||
validates :total_amount, numericality: true
|
||||
|
||||
def save
|
||||
payment = Quickbooks::Model::Payment.new
|
||||
payment.customer_id = @customer_id.to_i
|
||||
payment.deposit_to_account_id = @account_id.to_i
|
||||
payment.payment_method_id = @payment_method_id.to_i
|
||||
payment.total = @total_amount
|
||||
Qbo.get_base(:payment).service.update(payment)
|
||||
end
|
||||
|
||||
def save!
|
||||
save
|
||||
end
|
||||
|
||||
# Dummy stub to make validtions happy.
|
||||
def update_attribute
|
||||
end
|
||||
|
||||
end
|
||||
@@ -10,54 +10,145 @@
|
||||
|
||||
class QboInvoice < ActiveRecord::Base
|
||||
unloadable
|
||||
has_many :issues
|
||||
attr_accessible :doc_number
|
||||
validates_presence_of :id, :doc_number
|
||||
|
||||
has_and_belongs_to_many :issues
|
||||
attr_accessible :doc_number, :id
|
||||
validates_presence_of :doc_number, :id
|
||||
self.primary_key = :id
|
||||
|
||||
def self.get_base
|
||||
Qbo.get_base(:invoice)
|
||||
Qbo.get_base(:invoice).service
|
||||
end
|
||||
|
||||
# sync ALL the invoices
|
||||
def self.sync
|
||||
#Pull the invoices from the quickbooks server
|
||||
#invoices = get_base.service.all
|
||||
|
||||
last = Qbo.first.last_sync
|
||||
|
||||
query = "SELECT Id, DocNumber FROM Invoice"
|
||||
query << " WHERE Metadata.LastUpdatedTime >= '#{last.iso8601}' " if last
|
||||
|
||||
if count == 0
|
||||
invoices = get_base.service.all
|
||||
invoices = get_base.all
|
||||
else
|
||||
invoices = get_base.service.query()
|
||||
invoices = get_base.query()
|
||||
end
|
||||
|
||||
# Update the invoice table
|
||||
invoices.each { | invoice |
|
||||
qbo_invoice = find_or_create_by(id: invoice.id)
|
||||
qbo_invoice.doc_number = invoice.doc_number
|
||||
qbo_invoice.id = invoice.id
|
||||
qbo_invoice.save!
|
||||
process_invoice invoice
|
||||
}
|
||||
|
||||
#remove deleted invoices
|
||||
#where.not(invoices.map(&:id)).destroy_all
|
||||
end
|
||||
|
||||
#sync by invoice ID
|
||||
def self.sync_by_id(id)
|
||||
invoice = get_base.service.fetch_by_id(id)
|
||||
qbo_invoice = find_or_create_by(id: invoice.id)
|
||||
#update the information in the database
|
||||
invoice = get_base.fetch_by_id(id)
|
||||
process_invoice invoice
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# Attach the invoice to the issue
|
||||
def self.attach_to_issue(issue, invoice)
|
||||
return if issue.nil?
|
||||
|
||||
# skip this issue if the issue customer is not the same as the invoice customer
|
||||
return if issue.customer_id != invoice.customer_ref.value.to_i
|
||||
|
||||
# Load the invoice into the database
|
||||
qbo_invoice = QboInvoice.find_or_create_by(id: invoice.id)
|
||||
qbo_invoice.doc_number = invoice.doc_number
|
||||
qbo_invoice.id = invoice.id
|
||||
qbo_invoice.save!
|
||||
qbo_invoice.save!
|
||||
|
||||
unless issue.qbo_invoices.include?(qbo_invoice)
|
||||
issue.qbo_invoices << qbo_invoice
|
||||
issue.save!
|
||||
end
|
||||
|
||||
compare_custom_fields(issue, invoice)
|
||||
end
|
||||
|
||||
def self.update(id)
|
||||
# Update the item table
|
||||
invoice = get_base.service.fetch_by_id(id)
|
||||
qbo_invoice = find_or_create_by(id: id)
|
||||
qbo_invoice.doc_number = invoice.doc_number
|
||||
qbo_invoice.save!
|
||||
# processes the invoice into the system
|
||||
def self.process_invoice(invoice)
|
||||
# Check the private notes
|
||||
if not invoice.private_note.nil?
|
||||
invoice.private_note.scan(/#(\w+)/).flatten.each { |issue|
|
||||
attach_to_issue(Issue.find_by_id(issue.to_i), invoice)
|
||||
}
|
||||
end
|
||||
|
||||
# Scan the line items for hashtags and attach to the applicable issues
|
||||
invoice.line_items.each { |line|
|
||||
if line.description
|
||||
line.description.scan(/#(\w+)/).flatten.each { |issue|
|
||||
attach_to_issue(Issue.find_by_id(issue.to_i), invoice)
|
||||
}
|
||||
end
|
||||
}
|
||||
end
|
||||
|
||||
def self.compare_custom_fields(issue, invoice)
|
||||
is_changed = false
|
||||
|
||||
# update the invoive custom fields with infomation from the work ticket if available
|
||||
invoice.custom_fields.each { |cf|
|
||||
|
||||
# TODO Add some hooks here
|
||||
|
||||
# VIN from the attached vehicle
|
||||
begin
|
||||
if cf.name.eql? "VIN"
|
||||
vin = Vehicle.find(issue.vehicles_id).vin
|
||||
break if vin.nil?
|
||||
if not cf.string_value.to_s.eql? vin
|
||||
cf.string_value = vin.to_s
|
||||
is_changed = true
|
||||
end
|
||||
end
|
||||
rescue
|
||||
#do nothing
|
||||
end
|
||||
|
||||
# Custom Values
|
||||
begin
|
||||
value = issue.custom_values.find_by(custom_field_id: CustomField.find_by_name(cf.name).id)
|
||||
|
||||
# Check to see if the value is blank...
|
||||
if not value.value.to_s.blank?
|
||||
# Check to see if the value is diffrent
|
||||
if not cf.string_value.to_s.eql? value.value.to_s
|
||||
|
||||
# Use the lowest Milage
|
||||
if cf.name.eql? "Mileage In"
|
||||
if cf.string_value.to_i > value.value.to_i or cf.string_value.blank?
|
||||
cf.string_value = value.value.to_s
|
||||
is_changed = true
|
||||
end
|
||||
end
|
||||
|
||||
# Use the max milage
|
||||
if cf.name.eql? "Mileage Out"
|
||||
if cf.string_value.to_i < value.value.to_i or cf.string_value.blank?
|
||||
cf.string_value = value.value.to_s
|
||||
is_changed = true
|
||||
end
|
||||
end
|
||||
|
||||
# Everything else
|
||||
cf.string_value = value.value.to_s
|
||||
is_changed = true
|
||||
end
|
||||
end
|
||||
rescue
|
||||
# Nothing to do here, there is no match
|
||||
end
|
||||
}
|
||||
|
||||
# TODO Add some hooks here
|
||||
|
||||
# Push updates
|
||||
get_base.update(invoice) if is_changed
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
@@ -72,7 +72,7 @@ class Vehicle < ActiveRecord::Base
|
||||
# Force Upper Case for VIN numbers
|
||||
def vin=(val)
|
||||
# The to_s is in case you get nil/non-string
|
||||
write_attribute(:vin, val.to_s.upcase)
|
||||
write_attribute(:vin, val.to_s.scan(/^[A-Za-z0-9]+$/).join.upcase)
|
||||
end
|
||||
|
||||
# search for a vin
|
||||
@@ -87,8 +87,8 @@ class Vehicle < ActiveRecord::Base
|
||||
if self.vin?
|
||||
begin
|
||||
@details = JSON.parse get_decoder.full(self.vin)
|
||||
raise @details['message'] if @details['status'] == "NOT_FOUND"
|
||||
raise @details['message'] if @details['status'] == "BAD_REQUEST"
|
||||
raise @details['message'] if @details['status'].to_s.eql? "NOT_FOUND"
|
||||
raise @details['message'] if @details['status'].to_s.eql? "BAD_REQUEST"
|
||||
rescue Exception => e
|
||||
errors.add(:vin, e.message)
|
||||
end
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>Customer</th>
|
||||
<td><%= customer.name %></td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<th>Email</th>
|
||||
<td><%= customer.email %></td>
|
||||
@@ -12,12 +7,37 @@
|
||||
|
||||
<tr>
|
||||
<th>Primary Phone</th>
|
||||
<td><%= customer.primary_phone %></td>
|
||||
<td><%= number_to_phone(customer.primary_phone.gsub(/[^\d]/, '').to_i, area_code: true) if customer.primary_phone %></td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<th>Mobile Phone</th>
|
||||
<td><%= customer.mobile_phone %></td>
|
||||
<td><%= number_to_phone(customer.mobile_phone.gsub(/[^\d]/, '').to_i, area_code: true) if customer.mobile_phone %></td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<th>Bill Address</th>
|
||||
<td><%= customer.billing_address %></td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<th>Shipping Address</th>
|
||||
<td><%= customer.shipping_address %></td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<th>Issues</th>
|
||||
<td><%= customer.issues.count %></td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<th>Account Balance</th>
|
||||
<td>$<%= customer.balance %></td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<th>Balance With Jobs</th>
|
||||
<td>$<%= customer.balance_with_jobs %></td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
@@ -26,12 +46,6 @@
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<th>Issues</th>
|
||||
<td><%= customer.issues.count %></td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td/>
|
||||
<td>
|
||||
<%= button_to "Edit Customer", edit_customer_path(customer), method: :get%>
|
||||
</td>
|
||||
|
||||
1
app/views/customers/filter_vehicles_by_customer.js.erb
Normal file
1
app/views/customers/filter_vehicles_by_customer.js.erb
Normal file
@@ -0,0 +1 @@
|
||||
$('select#issue_vehicles_id').html('<%= j options_from_collection_for_select(@filtered_vehicles, :id, :to_s) %>');
|
||||
@@ -3,19 +3,17 @@
|
||||
<%= form_tag(customers_path, :method => "get", id: "search-form") do %>
|
||||
<%= text_field_tag :search, params[:search], placeholder: "Search Customers" %>
|
||||
<%= submit_tag "Search" %>
|
||||
<%= button_to "New Customer", new_customer_path, method: :get %>
|
||||
<% end %>
|
||||
<br/>
|
||||
<% if @customers.present? %>
|
||||
|
||||
<br/>
|
||||
<% @customers.each do |c| %>
|
||||
<div class="row">
|
||||
<div class="span6 columns">
|
||||
<%= link_to c, customer_path(c.id) %>
|
||||
<br/>
|
||||
<% @customers.each do |c| %>
|
||||
<div class="row">
|
||||
<div class="span6 columns">
|
||||
<%= link_to c, customer_path(c.id) %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
<div class="actions">
|
||||
<%= will_paginate @customers %>
|
||||
@@ -26,5 +24,5 @@
|
||||
<% end %>
|
||||
|
||||
<div>
|
||||
<b>Last Sync: </b> <%= Qbo.last_sync %>
|
||||
<%= Customer.count %> Customers - <b>Last Sync: </b> <%= Qbo.last_sync if Qbo.exists? %>
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,27 @@
|
||||
<h1>Customer #<%= @customer.id %></h1>
|
||||
<br/>
|
||||
<%= render :partial => 'customers/details', locals: {customer: @customer} %>
|
||||
<br/>
|
||||
<%= render :partial => 'vehicles/list' %>
|
||||
<br/>
|
||||
<%= render :partial => 'issues/list_simple', locals: {issues: @issues} %>
|
||||
<div id="content">
|
||||
<h2>Customer #<%= @customer.id %></h2>
|
||||
<br/>
|
||||
|
||||
<div class="subject">
|
||||
<div><h3><%= @customer.name %></h3></div>
|
||||
</div>
|
||||
|
||||
<div class="attributes">
|
||||
|
||||
<div class="splitcontent">
|
||||
<div class="splitcontentleft">
|
||||
<h4>Details:</h4>
|
||||
<%= render :partial => 'customers/details', locals: {customer: @customer} %>
|
||||
</div>
|
||||
<div class="splitcontentleft">
|
||||
<h4>Vehicles:</h4>
|
||||
<%= render :partial => 'vehicles/list' %>
|
||||
<%= button_to "New Vehicle", new_customer_vehicle_path(@customer), method: :get %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<br/>
|
||||
<h2>Issues:</h2>
|
||||
<%= render :partial => 'issues/list_simple', locals: {issues: @issues} %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
109
app/views/customers/view.html.erb
Normal file
109
app/views/customers/view.html.erb
Normal file
@@ -0,0 +1,109 @@
|
||||
<h2><%= issue_heading(@issue) %></h2>
|
||||
|
||||
<div class="<%= @issue.css_classes %> details">
|
||||
|
||||
<%= avatar(@issue.author, :size => "50") %>
|
||||
|
||||
<div class="subject">
|
||||
<%= render_issue_subject_with_tree(@issue) %>
|
||||
This customer link expires in <%= distance_of_time_in_words(Time.now, @token.expires_at) %>
|
||||
</div>
|
||||
<p class="author">
|
||||
<%= authoring @issue.created_on, @issue.author %>.
|
||||
<% if @issue.created_on != @issue.updated_on %>
|
||||
<%= l(:label_updated_time, time_tag(@issue.updated_on)).html_safe %>.
|
||||
<% end %>
|
||||
</p>
|
||||
|
||||
<div class="attributes">
|
||||
<%= issue_fields_rows do |rows|
|
||||
rows.left l(:field_status), @issue.status.name, :class => 'status'
|
||||
rows.left l(:field_priority), @issue.priority.name, :class => 'priority'
|
||||
unless @issue.disabled_core_fields.include?('assigned_to_id')
|
||||
rows.left l(:field_assigned_to), avatar(@issue.assigned_to, :size => "14").to_s.html_safe + (@issue.assigned_to ? link_to_user(@issue.assigned_to) : "-"), :class => 'assigned-to'
|
||||
end
|
||||
unless @issue.disabled_core_fields.include?('category_id') || (@issue.category.nil? && @issue.project.issue_categories.none?)
|
||||
rows.left l(:field_category), (@issue.category ? @issue.category.name : "-"), :class => 'category'
|
||||
end
|
||||
unless @issue.disabled_core_fields.include?('fixed_version_id') || (@issue.fixed_version.nil? && @issue.assignable_versions.none?)
|
||||
rows.left l(:field_fixed_version), (@issue.fixed_version ? link_to_version(@issue.fixed_version) : "-"), :class => 'fixed-version'
|
||||
end
|
||||
unless @issue.disabled_core_fields.include?('start_date')
|
||||
rows.right l(:field_start_date), format_date(@issue.start_date), :class => 'start-date'
|
||||
end
|
||||
unless @issue.disabled_core_fields.include?('due_date')
|
||||
rows.right l(:field_due_date), format_date(@issue.due_date), :class => 'due-date'
|
||||
end
|
||||
unless @issue.disabled_core_fields.include?('done_ratio')
|
||||
rows.right l(:field_done_ratio), progress_bar(@issue.done_ratio, :legend => "#{@issue.done_ratio}%"), :class => 'progress'
|
||||
end
|
||||
unless @issue.disabled_core_fields.include?('estimated_hours')
|
||||
if @issue.estimated_hours.present? || @issue.total_estimated_hours.to_f > 0
|
||||
rows.right l(:field_estimated_hours), issue_estimated_hours_details(@issue), :class => 'estimated-hours'
|
||||
end
|
||||
end
|
||||
#if User.current.allowed_to_view_all_time_entries?(@project)
|
||||
if @issue.total_spent_hours > 0
|
||||
rows.right l(:label_spent_time), issue_spent_hours_details(@issue), :class => 'spent-time'
|
||||
end
|
||||
#end
|
||||
end %>
|
||||
<%= render_custom_fields_rows(@issue) %>
|
||||
<%= call_hook(:view_issues_show_details_bottom, :issue => @issue) %>
|
||||
</div>
|
||||
|
||||
<% if @issue.description? || @issue.attachments.any? -%>
|
||||
<hr />
|
||||
<% if @issue.description? %>
|
||||
<div class="description">
|
||||
<div class="contextual">
|
||||
<%= link_to l(:button_quote), quoted_issue_path(@issue), :remote => true, :method => 'post', :class => 'icon icon-comment' if @issue.notes_addable? %>
|
||||
</div>
|
||||
|
||||
<p><strong><%=l(:field_description)%></strong></p>
|
||||
<div class="wiki">
|
||||
<%= textilizable @issue, :description, :attachments => @issue.attachments %>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
<%= link_to_attachments @issue, :thumbnails => true %>
|
||||
<% end -%>
|
||||
|
||||
<%= call_hook(:view_issues_show_description_bottom, :issue => @issue) %>
|
||||
|
||||
<% if !@issue.leaf? || User.current.allowed_to?(:manage_subtasks, @project) %>
|
||||
<hr />
|
||||
<div id="issue_tree">
|
||||
<div class="contextual">
|
||||
<%= link_to_new_subtask(@issue) if User.current.allowed_to?(:manage_subtasks, @project) %>
|
||||
</div>
|
||||
<p><strong><%=l(:label_subtask_plural)%></strong></p>
|
||||
<%= render_descendants_tree(@issue) unless @issue.leaf? %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<% if @relations.present? || User.current.allowed_to?(:manage_issue_relations, @project) %>
|
||||
<hr />
|
||||
<div id="relations">
|
||||
<%= render :partial => 'issues/relations' %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
</div>
|
||||
|
||||
<% if @changesets.present? %>
|
||||
<div id="issue-changesets">
|
||||
<h3><%=l(:label_associated_revisions)%></h3>
|
||||
<%= render :partial => 'issues/changesets', :locals => { :changesets => @changesets} %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<% if @journals.present? %>
|
||||
<div id="history">
|
||||
<h3><%=l(:label_history)%></h3>
|
||||
<%= render :partial => 'issues/history', :locals => { :issue => @issue, :journals => @journals } %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
|
||||
<% html_title "#{@issue.tracker.name} ##{@issue.id}: #{@issue.subject}" %>
|
||||
42
app/views/payments/_form.html.erb
Normal file
42
app/views/payments/_form.html.erb
Normal file
@@ -0,0 +1,42 @@
|
||||
<div class="row">
|
||||
<div class="span6 columns">
|
||||
<fieldset>
|
||||
|
||||
<%= form_for @payment do |f| %>
|
||||
|
||||
<div class="clearfix">
|
||||
Customer:
|
||||
<div class="input">
|
||||
<%= f.collection_select :customer_id, @customers, :id, :name, include_blank: true, :selected => @customer, :required => true%>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="clearfix">
|
||||
Deposit to Account:
|
||||
<div class="input">
|
||||
<%= f.collection_select :account_id, @accounts, :id, :name, include_blank: true, :selected => @account, :required => true%>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="clearfix">
|
||||
Payment Method:
|
||||
<div class="input">
|
||||
<%= f.collection_select :payment_method_id, @payment_methods, :id, :name, include_blank: true, :selected => @payment_method, :required => true%>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="clearfix">
|
||||
Amount:
|
||||
<div class="input">
|
||||
<%= f.number_field :total_amount %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="actions">
|
||||
<%= f.submit %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
</fieldset>
|
||||
</div>
|
||||
</div>
|
||||
3
app/views/payments/new.html.erb
Normal file
3
app/views/payments/new.html.erb
Normal file
@@ -0,0 +1,3 @@
|
||||
<h1>New Payment</h1>
|
||||
<br/>
|
||||
<%= render :partial => 'payments/form' %>
|
||||
@@ -72,6 +72,36 @@ intuit.ipp.anywhere.setup({menuProxy: '/path/to/blue-dot', grantUrl: '<%= qbo_au
|
||||
<br/>
|
||||
Note: You need to authenticate after saving your key and secret above
|
||||
<br/>
|
||||
<br/>
|
||||
|
||||
<!-- this will display a button that the user clicks to start the flow -->
|
||||
<ipp:connectToIntuit></ipp:connectToIntuit>
|
||||
|
||||
<br/>
|
||||
<br/>
|
||||
|
||||
<div>
|
||||
<b>Customer Count:</b> <%= Customer.count%>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<b>Item Count:</b> <%= QboItem.count %>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<b>Employee Count:</b> <%= QboEmployee.count %>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<b>Invoice Count:</b> <%= QboInvoice.count %>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<b>Estimate Count:</b> <%= QboEstimate.count %>
|
||||
</div>
|
||||
|
||||
<br/>
|
||||
|
||||
<div>
|
||||
<b>Last Sync: </b> <%= Qbo.last_sync if Qbo.exists? %> <%= link_to " Sync Now", qbo_sync_path %>
|
||||
</div>
|
||||
|
||||
@@ -36,7 +36,7 @@ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLI
|
||||
<br/>
|
||||
|
||||
<div>
|
||||
<b>Last Sync: </b> <%= Qbo.last_sync %>
|
||||
<b>Last Sync: </b> <%= Qbo.last_sync if Qbo.exists? %>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
<tr>
|
||||
<th>Customer</th>
|
||||
<td><%= vehicle.customer.name %></td>
|
||||
<td><%= link_to vehicle.customer.name, customer_path(vehicle.customer) %></td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
<%= @f.collection_select :vehicle_id, @customer.vehicles.order(:year), :id, :vin, include_blank: true, :selected => @vehicle%>
|
||||
Partial Test
|
||||
@@ -34,25 +34,21 @@
|
||||
<div class="clearfix">
|
||||
VIN:
|
||||
<div class="input">
|
||||
<%= f.text_field :vin %>
|
||||
<%= f.text_field :vin , :autofocus => true %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="clearfix">
|
||||
Notes:
|
||||
<div class="input">
|
||||
<p>
|
||||
<%= link_to_function content_tag(:span, l(:button_edit), :class => 'icon icon-edit'), '$(this).hide(); $("#issue_description_and_toolbar").show()' unless @vehicle.new_record? %>
|
||||
<p>
|
||||
<%= content_tag 'span', :id => "issue_description_and_toolbar", :style => (@vehicle.new_record? ? nil : 'display:none') do %>
|
||||
<%= f.text_area :notes,
|
||||
<%= f.text_area :notes,
|
||||
:cols => 60,
|
||||
:rows => 10,
|
||||
:accesskey => accesskey(:edit),
|
||||
:class => 'wiki-edit',
|
||||
:no_label => true %>
|
||||
<% end %>
|
||||
</p>
|
||||
<%= wikitoolbar_for 'issue_description' %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -16,8 +16,6 @@
|
||||
</div>
|
||||
<br/>
|
||||
<% end %>
|
||||
|
||||
<br/>
|
||||
|
||||
<div class="actions">
|
||||
<%= will_paginate @vehicles %>
|
||||
|
||||
1
app/views/vehicles/_vehicle.html.erb
Normal file
1
app/views/vehicles/_vehicle.html.erb
Normal file
@@ -0,0 +1 @@
|
||||
<option value="<%= vehicle.id %>"><%= vehicle.to_s.titleize %></option>
|
||||
@@ -2,9 +2,8 @@
|
||||
<br/>
|
||||
|
||||
<%= form_tag(vehicles_path, :method => "get", id: "search-form") do %>
|
||||
<%= text_field_tag :search, params[:search], placeholder: "Search Vehicles by VIN" %>
|
||||
<%= submit_tag "Search" %>
|
||||
<%= button_to "New Vehicle", new_vehicle_path, method: :get %>
|
||||
<%= text_field_tag :search, params[:search], placeholder: "Search Vehicles by VIN" %>
|
||||
<%= submit_tag "Search" %>
|
||||
<% end %>
|
||||
|
||||
<%= render :partial => 'vehicles/list' %>
|
||||
|
||||
18
app/workers/email_worker.rb
Normal file
18
app/workers/email_worker.rb
Normal file
@@ -0,0 +1,18 @@
|
||||
#The MIT License (MIT)
|
||||
#
|
||||
#Copyright (c) 2017 rick barrette
|
||||
#
|
||||
#Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
#
|
||||
#The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
#
|
||||
#THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
# This sidekiq worker class will handle emailing weekly time reports
|
||||
class EmailWorker
|
||||
include Sidekiq::Worker
|
||||
|
||||
def perform()
|
||||
# email something
|
||||
end
|
||||
end
|
||||
9
assets/javascripts/application.js
Normal file
9
assets/javascripts/application.js
Normal file
@@ -0,0 +1,9 @@
|
||||
$(function() {
|
||||
$("input#issue_customer_id").on("change", function() {
|
||||
$.ajax({
|
||||
url: "/filter_vehicles_by_customer",
|
||||
type: "GET",
|
||||
data: { selected_customer: $("input#issue_customer_id").val() }
|
||||
});
|
||||
});
|
||||
});
|
||||
1
assets/javascripts/autocomplete-rails.js
Normal file
1
assets/javascripts/autocomplete-rails.js
Normal file
@@ -0,0 +1 @@
|
||||
!function(t){t.fn.railsAutocomplete=function(e){var a=function(){this.railsAutoCompleter||(this.railsAutoCompleter=new t.railsAutocomplete(this))};if(void 0!==t.fn.on){if(!e)return;return t(document).on("focus",e,a)}return this.live("focus",a)},t.railsAutocomplete=function(t){var e=t;this.init(e)},t.railsAutocomplete.options={showNoMatches:!0,noMatchesLabel:"no existing match"},t.railsAutocomplete.fn=t.railsAutocomplete.prototype={railsAutocomplete:"0.0.1"},t.railsAutocomplete.fn.extend=t.railsAutocomplete.extend=t.extend,t.railsAutocomplete.fn.extend({init:function(e){function a(t){return t.split(e.delimiter)}function i(t){return a(t).pop().replace(/^\s+/,"")}e.delimiter=t(e).attr("data-delimiter")||null,e.min_length=t(e).attr("data-min-length")||t(e).attr("min-length")||2,e.append_to=t(e).attr("data-append-to")||null,e.autoFocus=t(e).attr("data-auto-focus")||!1,t(e).autocomplete({appendTo:e.append_to,autoFocus:e.autoFocus,delay:t(e).attr("delay")||0,source:function(a,r){var n=this.element[0],o={term:i(a.term)};t(e).attr("data-autocomplete-fields")&&t.each(t.parseJSON(t(e).attr("data-autocomplete-fields")),function(e,a){o[e]=t(a).val()}),t.getJSON(t(e).attr("data-autocomplete"),o,function(){var a={};t.extend(a,t.railsAutocomplete.options),t.each(a,function(i,r){if(a.hasOwnProperty(i)){var n=t(e).attr("data-"+i);a[i]=n?n:r}}),0==arguments[0].length&&t.inArray(a.showNoMatches,[!0,"true"])>=0&&(arguments[0]=[],arguments[0][0]={id:"",label:a.noMatchesLabel}),t(arguments[0]).each(function(a,i){var r={};r[i.id]=i,t(e).data(r)}),r.apply(null,arguments),t(n).trigger("railsAutocomplete.source",arguments)})},change:function(e,a){if(t(this).is("[data-id-element]")&&""!==t(t(this).attr("data-id-element")).val()&&(t(t(this).attr("data-id-element")).val(a.item?a.item.id:"").trigger("change"),t(this).attr("data-update-elements"))){var i=t.parseJSON(t(this).attr("data-update-elements")),r=a.item?t(this).data(a.item.id.toString()):{};if(i&&""===t(i.id).val())return;for(var n in i){var o=t(i[n]);o.is(":checkbox")?null!=r[n]&&o.prop("checked",r[n]):o.val(a.item?r[n]:"").trigger("change")}}},search:function(){var t=i(this.value);return t.length<e.min_length?!1:void 0},focus:function(){return!1},select:function(i,r){if(r.item.value=r.item.value.toString(),-1!=r.item.value.toLowerCase().indexOf("no match")||-1!=r.item.value.toLowerCase().indexOf("too many results"))return t(this).trigger("railsAutocomplete.noMatch",r),!1;var n=a(this.value);if(n.pop(),n.push(r.item.value),null!=e.delimiter)n.push(""),this.value=n.join(e.delimiter);else if(this.value=n.join(""),t(this).attr("data-id-element")&&t(t(this).attr("data-id-element")).val(r.item.id).trigger("change"),t(this).attr("data-update-elements")){var o=r.item,l=-1!=r.item.value.indexOf("Create New")?!0:!1,u=t.parseJSON(t(this).attr("data-update-elements"));for(var s in u)"checkbox"===t(u[s]).attr("type")?o[s]===!0||1===o[s]?t(u[s]).attr("checked","checked"):t(u[s]).removeAttr("checked"):l&&o[s]&&-1==o[s].indexOf("Create New")||!l?t(u[s]).val(o[s]).trigger("change"):t(u[s]).val("").trigger("change")}var c=this.value;return t(this).bind("keyup.clearId",function(){t.trim(t(this).val())!=t.trim(c)&&(t(t(this).attr("data-id-element")).val("").trigger("change"),t(this).unbind("keyup.clearId"))}),t(e).trigger("railsAutocomplete.select",r),!1}}),t(e).trigger("railsAutocomplete.init")}}),t(document).ready(function(){t("input[data-autocomplete]").railsAutocomplete("input[data-autocomplete]")})}(jQuery);
|
||||
@@ -19,3 +19,5 @@ en:
|
||||
field_vehicles: "Vehicle"
|
||||
field_vin: "VIN"
|
||||
field_notes: "Notes"
|
||||
field_qbo_billed: "Billed"
|
||||
label_week: "Week"
|
||||
|
||||
@@ -8,17 +8,41 @@
|
||||
#
|
||||
#THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
# Plugin's routes
|
||||
# See: http://guides.rubyonrails.org/routing.html
|
||||
#
|
||||
# Main Quickbooks landing page
|
||||
get 'qbo', :to=> 'qbo#index'
|
||||
|
||||
#authentication
|
||||
get 'qbo/authenticate', :to => 'qbo#authenticate'
|
||||
get 'qbo/oauth_callback', :to => 'qbo#oauth_callback'
|
||||
|
||||
#manual sync
|
||||
get 'qbo/sync', :to => 'qbo#sync'
|
||||
|
||||
# Estimate & Invoice PDF
|
||||
get 'qbo/estimate/:id', :to => 'estimate#show', as: :estimate
|
||||
get 'qbo/invoice/:id', :to => 'invoice#show', as: :invoice
|
||||
|
||||
#manual billing
|
||||
get 'qbo/bill/:id', :to => 'qbo#bill', as: :bill
|
||||
|
||||
#customer issue view
|
||||
get 'customers/view/:token', :to => 'customers#view', as: :view
|
||||
|
||||
#payments
|
||||
resources :payments
|
||||
|
||||
#webhook
|
||||
post 'qbo/webhook', :to => 'qbo#qbo_webhook'
|
||||
|
||||
#ajax
|
||||
get 'filter_vehicles_by_customer' => 'customers#filter_vehicles_by_customer'
|
||||
|
||||
# Nest Vehicles under customers
|
||||
resources :customers do
|
||||
resources :vehicles
|
||||
get :autocomplete_customer_name, :on => :collection
|
||||
get :autocomplete_customer_vehicles, :on => :collection
|
||||
end
|
||||
|
||||
#allow for just vehicles too
|
||||
resources :vehicles
|
||||
resources :customers
|
||||
|
||||
27
db/migrate/021_add_issues_qbo_invoices.rb
Normal file
27
db/migrate/021_add_issues_qbo_invoices.rb
Normal file
@@ -0,0 +1,27 @@
|
||||
#The MIT License (MIT)
|
||||
#
|
||||
#Copyright (c) 2016 rick barrette
|
||||
#
|
||||
#Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
#
|
||||
#The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
#
|
||||
#THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
class AddIssuesQboInvoices < ActiveRecord::Migration
|
||||
def self.up
|
||||
create_table :issues_qbo_invoices, :id => false do |t|
|
||||
t.references :issue
|
||||
t.references :qbo_invoice
|
||||
end
|
||||
|
||||
add_index :issues_qbo_invoices, [:issue_id, :qbo_invoice_id], :unique => true
|
||||
|
||||
# Now populate it with a SQL one-liner!
|
||||
execute "insert into issues_qbo_invoices(issue_id, qbo_invoice_id) select id, qbo_invoice_id from issues"
|
||||
end
|
||||
|
||||
def self.down
|
||||
drop_table :issues_qbo_invoices
|
||||
end
|
||||
end
|
||||
@@ -8,10 +8,8 @@
|
||||
#
|
||||
#THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
module ActiveSupport::Callbacks::ClassMethods
|
||||
def without_callback(*args, &block)
|
||||
skip_callback(*args)
|
||||
yield
|
||||
set_callback(*args)
|
||||
class UpdateIssuesRemoveInvoice < ActiveRecord::Migration
|
||||
def change
|
||||
remove_reference :issues, :qbo_invoice
|
||||
end
|
||||
end
|
||||
19
db/migrate/023_create_customer_tokens.rb
Normal file
19
db/migrate/023_create_customer_tokens.rb
Normal file
@@ -0,0 +1,19 @@
|
||||
#The MIT License (MIT)
|
||||
#
|
||||
#Copyright (c) 2016 rick barrette
|
||||
#
|
||||
#Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
#
|
||||
#The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
#
|
||||
#THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
class CreateCustomerTokens < ActiveRecord::Migration
|
||||
def change
|
||||
create_table :customer_tokens do |t|
|
||||
t.string :token
|
||||
t.timestamp :expires_at
|
||||
t.references :issue
|
||||
end
|
||||
end
|
||||
end
|
||||
23
init.rb
23
init.rb
@@ -15,21 +15,24 @@ Redmine::Plugin.register :redmine_qbo do
|
||||
require_dependency 'issues_save_hook_listener'
|
||||
require_dependency 'issues_show_hook_listener'
|
||||
require_dependency 'users_show_hook_listener'
|
||||
require_dependency 'header_footer_hook_listener'
|
||||
|
||||
# Patches to the Redmine core. Will not work in development mode
|
||||
require_dependency 'issue_patch'
|
||||
require_dependency 'user_patch'
|
||||
require_dependency 'query_patch'
|
||||
require_dependency 'time_entry_query_patch'
|
||||
require_dependency 'pdf_patch'
|
||||
require_dependency 'attachments_controller_patch'
|
||||
|
||||
name 'Redmine Quickbooks Online plugin'
|
||||
author 'Rick Barrette'
|
||||
description 'This is a plugin for Redmine to intergrate with Quickbooks Online to allow for seamless intergration CRM and invoicing of completed issues'
|
||||
version '0.1.0'
|
||||
version '0.4.2'
|
||||
url 'https://github.com/rickbarrette/redmine_qbo'
|
||||
author_url 'http://rickbarrette.org'
|
||||
settings :default => {'empty' => true}, :partial => 'qbo/settings'
|
||||
|
||||
|
||||
# Add safe attributes
|
||||
Issue.safe_attributes 'customer_id'
|
||||
Issue.safe_attributes 'qbo_item_id'
|
||||
@@ -41,13 +44,21 @@ Redmine::Plugin.register :redmine_qbo do
|
||||
|
||||
# We are playing in the sandbox
|
||||
#Quickbooks.sandbox_mode = true
|
||||
|
||||
|
||||
# set per_page globally
|
||||
WillPaginate.per_page = 10
|
||||
|
||||
# Register QBO top menu item
|
||||
menu :top_menu, :qbo, { :controller => :qbo, :action => :index }, :caption => 'Quickbooks', :if => Proc.new { User.current.admin? }
|
||||
menu :top_menu, :vehicles, { :controller => :vehicles, :action => :index }, :caption => 'Vehicles', :if => Proc.new { User.current.logged? }
|
||||
#menu :top_menu, :qbo, { :controller => :qbo, :action => :index }, :caption => 'Quickbooks', :if => Proc.new { User.current.admin? }
|
||||
menu :top_menu, :customers, { :controller => :customers, :action => :index }, :caption => 'Customers', :if => Proc.new { User.current.logged? }
|
||||
|
||||
menu :top_menu, :vehicles, { :controller => :vehicles, :action => :index }, :caption => 'Vehicles', :if => Proc.new { User.current.logged? }
|
||||
|
||||
menu :application_menu, :new_customer, { :controller => :customers, :action => :new }, :caption => 'New Customer', :if => Proc.new { User.current.logged? }
|
||||
menu :application_menu, :new_payment, { :controller => :payments, :action => :new }, :caption => 'New Payment', :if => Proc.new { User.current.logged? }
|
||||
|
||||
permission :customers, { :customers => [:index, :new] }, :public => false
|
||||
menu :project_menu, :customers, { :controller => 'customers', :action => 'new' }, :caption => 'New Customer', :after => :new_issue, :param => :project_id
|
||||
|
||||
permission :payments, { :payments => [:index, :new] }, :public => false
|
||||
menu :project_menu, :payments, { :controller => 'payments', :action => 'new' }, :caption => 'New Payment', :after => :customers, :param => :project_id
|
||||
end
|
||||
|
||||
38
lib/attachments_controller_patch.rb
Normal file
38
lib/attachments_controller_patch.rb
Normal file
@@ -0,0 +1,38 @@
|
||||
#The MIT License (MIT)
|
||||
#
|
||||
#Copyright (c) 2016 rick barrette
|
||||
#
|
||||
#Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
#
|
||||
#The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
#
|
||||
#THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
require_dependency 'attachments_controller'
|
||||
|
||||
module AttachmentsControllerPatch
|
||||
|
||||
def self.included(base) # :nodoc:
|
||||
base.extend(ClassMethods)
|
||||
|
||||
base.send(:include, InstanceMethods)
|
||||
|
||||
# Same as typing in the class
|
||||
base.class_eval do
|
||||
unloadable # Send unloadable so it will not be unloaded in development
|
||||
|
||||
skip_before_action :read_authorize
|
||||
end
|
||||
end
|
||||
|
||||
module ClassMethods
|
||||
|
||||
end
|
||||
|
||||
module InstanceMethods
|
||||
|
||||
end
|
||||
end
|
||||
|
||||
# Add module to AttachmentsController
|
||||
AttachmentsController.send(:include, AttachmentsControllerPatch)
|
||||
19
lib/header_footer_hook_listener.rb
Normal file
19
lib/header_footer_hook_listener.rb
Normal file
@@ -0,0 +1,19 @@
|
||||
#The MIT License (MIT)
|
||||
#
|
||||
#Copyright (c) 2016 rick barrette
|
||||
#
|
||||
#Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
#
|
||||
#The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
#
|
||||
#THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
class HeaderFooterHookListener < Redmine::Hook::ViewListener
|
||||
|
||||
def view_layouts_base_html_head(context = {})
|
||||
#nothing
|
||||
end
|
||||
|
||||
def view_layouts_base_body_bottom(context = {})
|
||||
return "<div id='qbo_footer' align='center'><b>Last Sync: </b> #{Qbo.last_sync if Qbo.exists?}</div>"
|
||||
end
|
||||
end
|
||||
@@ -23,9 +23,11 @@ module IssuePatch
|
||||
base.class_eval do
|
||||
unloadable # Send unloadable so it will not be unloaded in development
|
||||
belongs_to :customer, primary_key: :id
|
||||
belongs_to :qbo_item, primary_key: :id
|
||||
belongs_to :customer_token, primary_key: :id
|
||||
belongs_to :qbo_estimate, primary_key: :id
|
||||
belongs_to :qbo_invoice, primary_key: :id
|
||||
has_and_belongs_to_many :qbo_invoices
|
||||
#, :association_foreign_key => 'issue_id', :class_name => 'Issue', :join_table => 'issues_qbo_invoices'
|
||||
|
||||
belongs_to :vehicle, primary_key: :id
|
||||
end
|
||||
|
||||
@@ -36,7 +38,62 @@ module IssuePatch
|
||||
end
|
||||
|
||||
module InstanceMethods
|
||||
|
||||
# Create billable time entries
|
||||
def bill_time
|
||||
|
||||
# Get unbilled time entries
|
||||
spent_time = time_entries.where(qbo_billed: [false, nil])
|
||||
spent_hours ||= spent_time.sum(:hours) || 0
|
||||
|
||||
if spent_hours > 0 then
|
||||
|
||||
# Prepare to create a new Time Activity
|
||||
time_service = Qbo.get_base(:time_activity).service
|
||||
item_service = Qbo.get_base(:item).service
|
||||
time_entry = Quickbooks::Model::TimeActivity.new
|
||||
|
||||
h = Hash.new(0)
|
||||
spent_time.each do |entry|
|
||||
# Lets tottal up each activity
|
||||
h[entry.activity.name] += entry.hours
|
||||
# update time entries billed status
|
||||
entry.qbo_billed = true
|
||||
entry.save
|
||||
end
|
||||
|
||||
h.each do |key, val|
|
||||
|
||||
# Convert float spent time to hours and minutes
|
||||
hours = val.to_i
|
||||
minutesDecimal = (( val - hours) * 60)
|
||||
minutes = minutesDecimal.to_i
|
||||
|
||||
item = item_service.query("SELECT * FROM Item WHERE Name = '#{key}' ").first
|
||||
next if item.nil?
|
||||
|
||||
time_entry.description = "#{tracker} ##{id}: #{subject} #{"(Partial @ #{done_ratio}%)" if not closed?}"
|
||||
# TODO entry.user.qbo_employee.id
|
||||
time_entry.employee_id = assigned_to.qbo_employee_id
|
||||
time_entry.customer_id = customer_id
|
||||
time_entry.billable_status = "Billable"
|
||||
time_entry.hours = hours
|
||||
time_entry.minutes = minutes
|
||||
time_entry.name_of = "Employee"
|
||||
time_entry.txn_date = Date.today
|
||||
time_entry.hourly_rate = item.unit_price
|
||||
time_entry.item_id = item.id
|
||||
time_entry.start_time = start_date
|
||||
time_entry.end_time = Time.now
|
||||
time_service.create(time_entry)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Create a shareable link for a customer
|
||||
def share_token
|
||||
CustomerToken.create(:expires_at => Time.now + 1.month, :issue_id => id)
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
@@ -10,6 +10,13 @@
|
||||
|
||||
class IssuesFormHookListener < Redmine::Hook::ViewListener
|
||||
|
||||
# Load the javascript
|
||||
def view_layouts_base_html_head(context = {})
|
||||
js = javascript_include_tag 'application', :plugin => 'redmine_qbo'
|
||||
js += javascript_include_tag 'autocomplete-rails', :plugin => 'redmine_qbo'
|
||||
return js
|
||||
end
|
||||
|
||||
# Edit Issue Form
|
||||
# Show a dropdown for quickbooks contacts
|
||||
def view_issues_form_details_bottom(context={})
|
||||
@@ -17,32 +24,25 @@ class IssuesFormHookListener < Redmine::Hook::ViewListener
|
||||
|
||||
# Check to see if there is a quickbooks user attached to the issue
|
||||
selected_customer = context[:issue].customer ? context[:issue].customer.id : nil
|
||||
selected_item = context[:issue].qbo_item ? context[:issue].qbo_item.id : nil
|
||||
selected_invoice = context[:issue].qbo_invoice ? context[:issue].qbo_invoice.id : nil
|
||||
selected_estimate = context[:issue].qbo_estimate ? context[:issue].qbo_estimate.id : nil
|
||||
selected_vehicle = context[:issue].vehicles_id ? context[:issue].vehicles_id : nil
|
||||
|
||||
# Load customer information without callbacks
|
||||
customer = Customer.find_by_id(selected_customer) if selected_customer
|
||||
select_customer = f.select :customer_id, Customer.all.pluck(:name, :id).sort, :selected => selected_customer, include_blank: true
|
||||
|
||||
# Generate the drop down list of quickbooks items
|
||||
select_item = f.select :qbo_item_id, QboItem.all.pluck(:name, :id).sort, :selected => selected_item, include_blank: true
|
||||
|
||||
# Generate the drop down list of quickbooks invoices
|
||||
select_invoice = f.select :qbo_invoice_id, QboInvoice.all.pluck(:doc_number, :id).sort! {|x, y| y <=> x}, :selected => selected_invoice, include_blank: true
|
||||
# Load customer information
|
||||
customer = Customer.find_by_id(selected_customer) if selected_customer
|
||||
search_customer = f.autocomplete_field :customer, autocomplete_customer_name_customers_path, :selected => selected_customer, :update_elements => {:id => '#issue_customer_id', :value => '#issue_customer'}
|
||||
customer_id = f.hidden_field :customer_id, :id => "issue_customer_id"
|
||||
|
||||
# Generate the drop down list of quickbooks extimates
|
||||
select_estimate = f.select :qbo_estimate_id, QboEstimate.all.pluck(:doc_number, :id).sort! {|x, y| y <=> x}, :selected => selected_estimate, include_blank: true
|
||||
|
||||
if context[:issue].customer
|
||||
vehicles = customer.vehicles.pluck(:name, :id).sort!
|
||||
vehicles = customer.vehicles.pluck(:name, :id).sort!
|
||||
else
|
||||
vehicles = Vehicle.all.order(:name).pluck(:name, :id)
|
||||
vehicles = [nil].compact
|
||||
end
|
||||
|
||||
vehicle = f.select :vehicles_id, vehicles, :selected => selected_vehicle, include_blank: true
|
||||
|
||||
return "<p>#{select_customer}</p> <p>#{select_item}</p> <p>#{select_invoice}</p> <p>#{select_estimate}</p> <p>#{vehicle}</p>"
|
||||
return "<p><label for=\"issue_customer\">Customer</label>#{search_customer} #{customer_id}</p> <p>#{select_estimate}</p> <p>#{vehicle}</p>"
|
||||
end
|
||||
end
|
||||
|
||||
@@ -15,7 +15,7 @@ class IssuesSaveHookListener < Redmine::Hook::ViewListener
|
||||
issue = context[:issue]
|
||||
|
||||
# Check to see if we have registered with QBO
|
||||
if Qbo.first && issue.customer && issue.qbo_item
|
||||
if Qbo.first && issue.customer && issue. qbo_item_id
|
||||
|
||||
# if this is a quote, lets create a new estimate based off estimated hours
|
||||
if issue.tracker.name = "Quote" && issue.status.name = "New" && issue.qbo_estimate
|
||||
@@ -44,10 +44,6 @@ class IssuesSaveHookListener < Redmine::Hook::ViewListener
|
||||
|
||||
# Add the line items to the estimate
|
||||
estimate.line_items << line_item
|
||||
|
||||
# Save the etimate to the issue
|
||||
#issue.qbo_estimate_id = estimate_base.service.create(estimate).id
|
||||
#issue.save!
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -55,55 +51,6 @@ class IssuesSaveHookListener < Redmine::Hook::ViewListener
|
||||
# Called After Issue Saved
|
||||
def controller_issues_edit_after_save(context={})
|
||||
issue = context[:issue]
|
||||
|
||||
if issue.assigned_to
|
||||
employee_id = issue.assigned_to.qbo_employee_id
|
||||
|
||||
# Check to see if we have registered with QBO and if the issue is closed.
|
||||
# If so then we need to create a new billable time activity for the customer
|
||||
bill_time(issue, employee_id) if Qbo.first && issue.customer && issue.qbo_item && employee_id && issue.status.is_closed?
|
||||
end
|
||||
end
|
||||
|
||||
# Create billable time entries
|
||||
def bill_time(issue, employee_id)
|
||||
|
||||
# Get unbilled time entries
|
||||
spent_time = issue.time_entries.where(qbo_billed: [false, nil])
|
||||
spent_hours ||= spent_time.sum(:hours) || 0
|
||||
|
||||
if spent_hours > 0 then
|
||||
|
||||
# Prepare to create a new Time Activity
|
||||
time_service = Qbo.get_base(:time_activity).service
|
||||
item_service = Qbo.get_base(:item).service
|
||||
time_entry = Quickbooks::Model::TimeActivity.new
|
||||
|
||||
# Convert float spent time to hours and minutes
|
||||
hours = spent_hours.to_i
|
||||
minutesDecimal = (( spent_hours - hours) * 60)
|
||||
minutes = minutesDecimal.to_i
|
||||
|
||||
# update time entries billed status
|
||||
spent_time.each do |entry|
|
||||
entry.qbo_billed = true
|
||||
entry.save
|
||||
end
|
||||
|
||||
item = item_service.fetch_by_id issue.qbo_item_id
|
||||
time_entry.description = "#{issue.tracker} ##{issue.id}: #{issue.subject}"
|
||||
time_entry.employee_id = employee_id
|
||||
time_entry.customer_id = issue.customer_id
|
||||
time_entry.billable_status = "Billable"
|
||||
time_entry.hours = hours
|
||||
time_entry.minutes = minutes
|
||||
time_entry.name_of = "Employee"
|
||||
time_entry.txn_date = Date.today
|
||||
time_entry.hourly_rate = item.unit_price
|
||||
time_entry.item_id = issue.qbo_item_id
|
||||
time_entry.start_time = issue.start_date
|
||||
time_entry.end_time = Time.now
|
||||
time_service.create(time_entry)
|
||||
end
|
||||
issue.bill_time if Qbo.first && issue.customer && issue.status.is_closed?
|
||||
end
|
||||
end
|
||||
|
||||
@@ -19,15 +19,12 @@ class IssuesShowHookListener < Redmine::Hook::ViewListener
|
||||
# Display the quickbooks contact in the issue
|
||||
def view_issues_show_details_bottom(context={})
|
||||
issue = context[:issue]
|
||||
|
||||
|
||||
# Check to see if there is a quickbooks user attached to the issue
|
||||
if issue.customer
|
||||
customer = link_to issue.customer.name, "#{Redmine::Utils::relative_url_root}/customers/#{issue.customer.id}"
|
||||
end
|
||||
|
||||
# Check to see if there is a quickbooks item attached to the issue
|
||||
item = issue.qbo_item ? issue.qbo_item.name : nil
|
||||
|
||||
# Estimate Number
|
||||
if issue.qbo_estimate
|
||||
estimate = issue.qbo_estimate.doc_number
|
||||
@@ -35,12 +32,14 @@ class IssuesShowHookListener < Redmine::Hook::ViewListener
|
||||
end
|
||||
|
||||
# Invoice Number
|
||||
if issue.qbo_invoice
|
||||
invoice = issue.qbo_invoice.doc_number
|
||||
invoice_link = link_to invoice, "#{Redmine::Utils::relative_url_root}/qbo/invoice/#{issue.qbo_invoice.id}", :target => "_blank"
|
||||
invoice_link = ""
|
||||
if issue.qbo_invoice_ids
|
||||
issue.qbo_invoice_ids.each do |i|
|
||||
invoice = QboInvoice.find i
|
||||
invoice_link = invoice_link + link_to( invoice.doc_number, "#{Redmine::Utils::relative_url_root}/qbo/invoice/#{i}", :target => "_blank").to_s + " "
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
begin
|
||||
v = Vehicle.find(issue.vehicles_id)
|
||||
vehicle = link_to v.to_s, "#{Redmine::Utils::relative_url_root}/vehicles/#{v.id}"
|
||||
@@ -53,45 +52,48 @@ class IssuesShowHookListener < Redmine::Hook::ViewListener
|
||||
split_vin = vin.scan(/.{1,9}/) if vin
|
||||
|
||||
return "
|
||||
<div class=\"attributes\">
|
||||
<div class=\"splitcontent\">
|
||||
|
||||
<div class=\"customer_id attribute\">
|
||||
<div class=\"label\"><span>Customer</span>:</div>
|
||||
<div class=\"value\">#{customer}</div>
|
||||
</div>
|
||||
|
||||
<div class=\"qbo_item_id attribute\">
|
||||
<div class=\"label\"><span>Item</span>:</div>
|
||||
<div class=\"value\">#{item}</div>
|
||||
</div>
|
||||
|
||||
<div class=\"qbo_estimate_id attribute\">
|
||||
<div class=\"label\"><span>Estimate</span>:</div>
|
||||
<div class=\"value\">#{estimate_link}</div>
|
||||
</div>
|
||||
|
||||
<div class=\"qbo_invoice_id attribute\">
|
||||
<div class=\"label\"><span>Invoice</span>:</div>
|
||||
<div class=\"value\">#{invoice_link}</div>
|
||||
<div class=\"splitcontentleft\">
|
||||
<div class=\"customer_id attribute\">
|
||||
<div class=\"label\"><span>Customer</span>:</div>
|
||||
<div class=\"value\">#{customer}</div>
|
||||
</div>
|
||||
|
||||
<div class=\"qbo_estimate_id attribute\">
|
||||
<div class=\"label\"><span>Estimate</span>:</div>
|
||||
<div class=\"value\">#{estimate_link}</div>
|
||||
</div>
|
||||
|
||||
<div class=\"qbo_invoice_id attribute\">
|
||||
<div class=\"label\"><span>Invoice</span>:</div>
|
||||
<div class=\"value\">#{invoice_link}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<br/>
|
||||
|
||||
<div class=\"vehicle attribute\">
|
||||
<div class=\"label\"><span>Vehicle</span>:</div>
|
||||
<div class=\"value\">#{vehicle}</div>
|
||||
</div>
|
||||
<div class=\"splitcontentleft\">
|
||||
<div class=\"vehicle attribute\">
|
||||
<div class=\"label\"><span>Vehicle</span>:</div>
|
||||
<div class=\"value\">#{vehicle}</div>
|
||||
</div>
|
||||
|
||||
<div class=\"vehicle_vin attribute\">
|
||||
<div class=\"label\"><span>VIN</span>:</div>
|
||||
<div class=\"value\">#{split_vin[0] if split_vin}<b>#{split_vin[1] if split_vin}</b></div>
|
||||
</div>
|
||||
|
||||
<div class=\"vehicle_vin attribute\">
|
||||
<div class=\"label\"><span>VIN</span>:</div>
|
||||
<div class=\"value\">#{split_vin[0] if split_vin}<b>#{split_vin[1] if split_vin}</b></div>
|
||||
</div>
|
||||
|
||||
<div class=\"vehicle_notes attribute\">
|
||||
<div class=\"label\"><span>Notes</span>:</div>
|
||||
<div class=\"value\">#{notes}</div>
|
||||
<div class=\"vehicle_notes attribute\">
|
||||
<div class=\"label\"><span>Notes</span>:</div>
|
||||
<div class=\"value\">#{notes}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>"
|
||||
end
|
||||
|
||||
def view_issues_show_description_bottom(context={})
|
||||
bill_button = button_to "Bill Time", "#{Redmine::Utils::relative_url_root}/qbo/bill/#{context[:issue].id}", method: :get if User.current.admin?
|
||||
share_button = button_to "Share", "#{Redmine::Utils::relative_url_root}/customers/view/#{context[:issue].share_token.token}", method: :get if User.current.logged?
|
||||
return "<br/> #{bill_button} #{share_button}"
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
@@ -51,7 +51,7 @@ module IssuesPdfHelperPatch
|
||||
vin = v ? v.vin : nil
|
||||
notes = v ? v.notes : nil
|
||||
left << [l(:field_vehicles), vehicle]
|
||||
left << [l(:field_vin), vin.gsub(/(.{9})/, '\1 ')]
|
||||
left << [l(:field_vin), vin ? vin.gsub(/(.{9})/, '\1 ') : nil]
|
||||
#left << [l(:field_notes), notes]
|
||||
|
||||
right = []
|
||||
|
||||
@@ -36,6 +36,7 @@ module QueryPatch
|
||||
unless @available_columns
|
||||
@available_columns = available_columns_without_qbo
|
||||
@available_columns << QueryColumn.new(:customer, :sortable => "#{Customer.table_name}.name", :groupable => true, :caption => :field_customer)
|
||||
@available_columns << QueryColumn.new(:qbo_billed, :sortable => "#{TimeEntry.table_name}.qbo_billed", :groupable => true, :caption => :field_qbo_billed)
|
||||
end
|
||||
@available_columns
|
||||
end
|
||||
|
||||
71
lib/time_entry_query_patch.rb
Normal file
71
lib/time_entry_query_patch.rb
Normal file
@@ -0,0 +1,71 @@
|
||||
#The MIT License (MIT)
|
||||
#
|
||||
#Copyright (c) 2016 rick barrette
|
||||
#
|
||||
#Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
#
|
||||
#The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
#
|
||||
#THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
require_dependency 'time_entry_query'
|
||||
|
||||
module TimeEntryQueryPatch
|
||||
|
||||
def self.included(base) # :nodoc:
|
||||
base.extend(ClassMethods)
|
||||
|
||||
base.send(:include, InstanceMethods)
|
||||
|
||||
# Same as typing in the class
|
||||
base.class_eval do
|
||||
unloadable # Send unloadable so it will not be unloaded in development
|
||||
|
||||
alias_method_chain :available_columns, :qbo_billed
|
||||
alias_method_chain :available_filters, :qbo_billed
|
||||
end
|
||||
end
|
||||
|
||||
module ClassMethods
|
||||
|
||||
end
|
||||
|
||||
module InstanceMethods
|
||||
|
||||
def available_columns_with_qbo_billed
|
||||
unless @available_columns
|
||||
@available_columns = available_columns_without_qbo
|
||||
@available_columns << QueryColumn.new(:qbo_billed, :sortable => "#{TimeEntry.table_name}.name", :groupable => true, :caption => :field_qbo_billed)
|
||||
end
|
||||
@available_columns
|
||||
end
|
||||
|
||||
def available_filters_with_qbo_billed
|
||||
unless @available_filters
|
||||
@available_filters = available_filters_without_qbo
|
||||
|
||||
#qbo_filters = {
|
||||
# :customer => {
|
||||
# :id => l(:field_qbo_billed),
|
||||
# :type => :boolean,
|
||||
# :order => @available_filters.size + 1},
|
||||
#}
|
||||
|
||||
qbo_filters = {
|
||||
"qbo_billed" => {
|
||||
:id => :qbo_billed,
|
||||
:type => :list_optional,
|
||||
:order => @available_filters.size + 1,
|
||||
#:values => Customer.find(:all).collect { |c| [c.name, c.id.to_s]}
|
||||
}
|
||||
}
|
||||
|
||||
@available_filters.merge!(qbo_filters)
|
||||
end
|
||||
@available_filters
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Add module to TimeEntryQuery
|
||||
TimeEntryQuery.send(:include, QueryPatch)
|
||||
9
test/unit/customer_token_test.rb
Normal file
9
test/unit/customer_token_test.rb
Normal file
@@ -0,0 +1,9 @@
|
||||
require File.expand_path('../../test_helper', __FILE__)
|
||||
|
||||
class CustomerTokenTest < ActiveSupport::TestCase
|
||||
|
||||
# Replace this with your real tests.
|
||||
def test_truth
|
||||
assert true
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user