mirror of
https://github.com/openfoodfoundation/openfoodnetwork
synced 2026-01-24 20:36:49 +00:00
Merge branch 'master' into 2-0-stable-jan-29th
This commit is contained in:
3
Gemfile
3
Gemfile
@@ -72,10 +72,11 @@ gem 'roadie-rails', '~> 1.1.1'
|
||||
gem 'figaro'
|
||||
gem 'blockenspiel'
|
||||
gem 'acts-as-taggable-on', '~> 3.4'
|
||||
gem 'paper_trail', '~> 3.0.8'
|
||||
gem 'paper_trail', '~> 5.2.3'
|
||||
gem 'diffy'
|
||||
gem 'skylight', '< 2.0'
|
||||
|
||||
gem 'combine_pdf'
|
||||
gem 'wicked_pdf'
|
||||
gem 'wkhtmltopdf-binary'
|
||||
|
||||
|
||||
30
Gemfile.lock
30
Gemfile.lock
@@ -59,7 +59,7 @@ GIT
|
||||
json (>= 1.7.7)
|
||||
kaminari (~> 0.14.1)
|
||||
money (= 5.1.1)
|
||||
paperclip (~> 3.4.1)
|
||||
paperclip (~> 3.0)
|
||||
paranoia (~> 1.3)
|
||||
rails (~> 3.2.14)
|
||||
ransack (= 0.7.2)
|
||||
@@ -222,6 +222,8 @@ GEM
|
||||
execjs
|
||||
coffee-script-source (1.10.0)
|
||||
colorize (0.8.1)
|
||||
combine_pdf (1.0.15)
|
||||
ruby-rc4 (>= 0.1.5)
|
||||
compass (1.0.3)
|
||||
chunky_png (~> 1.2)
|
||||
compass-core (~> 1.0.2)
|
||||
@@ -242,7 +244,7 @@ GEM
|
||||
safe_yaml (~> 1.0.0)
|
||||
css_parser (1.6.0)
|
||||
addressable
|
||||
daemons (1.2.6)
|
||||
daemons (1.3.1)
|
||||
dalli (2.7.2)
|
||||
database_cleaner (0.7.1)
|
||||
db2fog (0.9.0)
|
||||
@@ -256,8 +258,8 @@ GEM
|
||||
rails (>= 3.1)
|
||||
delayed_job (4.1.5)
|
||||
activesupport (>= 3.0, < 5.3)
|
||||
delayed_job_active_record (4.1.2)
|
||||
activerecord (>= 3.0, < 5.2)
|
||||
delayed_job_active_record (4.1.3)
|
||||
activerecord (>= 3.0, < 5.3)
|
||||
delayed_job (>= 3.0, < 5)
|
||||
devise (2.2.8)
|
||||
bcrypt-ruby (~> 3.0)
|
||||
@@ -267,7 +269,7 @@ GEM
|
||||
devise-encryptable (0.1.2)
|
||||
devise (>= 2.1.0)
|
||||
diff-lcs (1.3)
|
||||
diffy (3.1.0)
|
||||
diffy (3.3.0)
|
||||
docile (1.3.1)
|
||||
dry-inflector (0.1.2)
|
||||
em-websocket (0.5.1)
|
||||
@@ -442,9 +444,9 @@ GEM
|
||||
foundation-icons-sass-rails (3.0.0)
|
||||
railties (>= 3.1.1)
|
||||
sass-rails (>= 3.1.1)
|
||||
foundation-rails (5.5.0.0)
|
||||
foundation-rails (5.5.2.1)
|
||||
railties (>= 3.1.0)
|
||||
sass (>= 3.2.0, < 3.4)
|
||||
sass (>= 3.3.0, < 3.5)
|
||||
fuubar (2.3.2)
|
||||
rspec-core (~> 3.0)
|
||||
ruby-progressbar (~> 1.4)
|
||||
@@ -541,11 +543,11 @@ GEM
|
||||
multi_json (~> 1.3)
|
||||
multi_xml (~> 0.5)
|
||||
rack (>= 1.2, < 3)
|
||||
oj (3.7.4)
|
||||
oj (3.7.8)
|
||||
orm_adapter (0.5.0)
|
||||
paper_trail (3.0.9)
|
||||
activerecord (>= 3.0, < 5.0)
|
||||
activesupport (>= 3.0, < 5.0)
|
||||
paper_trail (5.2.3)
|
||||
activerecord (>= 3.0, < 6.0)
|
||||
request_store (~> 1.1)
|
||||
paperclip (3.4.2)
|
||||
activemodel (>= 3.0.0)
|
||||
activerecord (>= 3.0.0)
|
||||
@@ -623,6 +625,8 @@ GEM
|
||||
json (~> 1.4)
|
||||
redcarpet (3.2.3)
|
||||
ref (2.0.0)
|
||||
request_store (1.4.1)
|
||||
rack (>= 1.4)
|
||||
roadie (3.4.0)
|
||||
css_parser (~> 1.4)
|
||||
nokogiri (~> 1.5)
|
||||
@@ -669,6 +673,7 @@ GEM
|
||||
unicode-display_width (~> 1.0, >= 1.0.1)
|
||||
ruby-ole (1.2.12.1)
|
||||
ruby-progressbar (1.10.0)
|
||||
ruby-rc4 (0.1.5)
|
||||
rubyzip (1.2.2)
|
||||
safe_yaml (1.0.4)
|
||||
sass (3.3.14)
|
||||
@@ -771,6 +776,7 @@ DEPENDENCIES
|
||||
capybara (>= 2.15.4)
|
||||
chromedriver-helper
|
||||
coffee-rails (~> 3.2.1)
|
||||
combine_pdf
|
||||
compass-rails
|
||||
custom_error_message!
|
||||
daemons
|
||||
@@ -811,7 +817,7 @@ DEPENDENCIES
|
||||
oauth2 (~> 1.4.1)
|
||||
ofn-qz!
|
||||
oj
|
||||
paper_trail (~> 3.0.8)
|
||||
paper_trail (~> 5.2.3)
|
||||
paperclip (~> 3.4.1)
|
||||
pg
|
||||
pry-byebug (>= 3.4.3)
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
angular.module("admin.orders").controller "bulkInvoiceCtrl", ($scope, $http, $timeout) ->
|
||||
$scope.createBulkInvoice = ->
|
||||
$scope.invoice_id = null
|
||||
$scope.poll = 1
|
||||
$scope.loading = true
|
||||
$scope.message = null
|
||||
$scope.error = null
|
||||
$scope.poll_wait = 5 # 5 Seconds between each check
|
||||
$scope.poll_retries = 80 # Maximum checks before stopping
|
||||
|
||||
$http.post('/admin/orders/invoices', {order_ids: $scope.selected_orders}).success (data) ->
|
||||
$scope.invoice_id = data
|
||||
$scope.pollBulkInvoice()
|
||||
|
||||
$scope.pollBulkInvoice = ->
|
||||
$timeout($scope.nextPoll, $scope.poll_wait * 1000)
|
||||
|
||||
$scope.nextPoll = ->
|
||||
$http.get('/admin/orders/invoices/'+$scope.invoice_id+'/poll').success (data) ->
|
||||
$scope.loading = false
|
||||
$scope.message = t('js.admin.orders.index.bulk_invoice_created')
|
||||
|
||||
.error (data) ->
|
||||
$scope.poll++
|
||||
|
||||
if $scope.poll > $scope.poll_retries
|
||||
$scope.loading = false
|
||||
$scope.error = t('js.admin.orders.index.bulk_invoice_failed')
|
||||
return
|
||||
|
||||
$scope.pollBulkInvoice()
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
angular.module("admin.orders").controller "ordersCtrl", ($scope, RequestMonitor, Orders, SortOptions) ->
|
||||
angular.module("admin.orders").controller "ordersCtrl", ($scope, RequestMonitor, Orders, SortOptions, $window, $filter) ->
|
||||
$scope.RequestMonitor = RequestMonitor
|
||||
$scope.pagination = Orders.pagination
|
||||
$scope.orders = Orders.all
|
||||
@@ -8,6 +8,11 @@ angular.module("admin.orders").controller "ordersCtrl", ($scope, RequestMonitor,
|
||||
{id: 50, name: t('js.admin.orders.index.per_page', results: 50)},
|
||||
{id: 100, name: t('js.admin.orders.index.per_page', results: 100)}
|
||||
]
|
||||
$scope.selected_orders = []
|
||||
$scope.checkboxes = {}
|
||||
$scope.selected = false
|
||||
$scope.select_all = false
|
||||
$scope.poll = 0
|
||||
|
||||
$scope.initialise = ->
|
||||
$scope.per_page = 15
|
||||
@@ -17,6 +22,7 @@ angular.module("admin.orders").controller "ordersCtrl", ($scope, RequestMonitor,
|
||||
$scope.fetchResults()
|
||||
|
||||
$scope.fetchResults = (page=1) ->
|
||||
$scope.resetSelected()
|
||||
Orders.index({
|
||||
'q[completed_at_lt]': $scope['q']['completed_at_lt'],
|
||||
'q[completed_at_gt]': $scope['q']['completed_at_gt'],
|
||||
@@ -35,6 +41,26 @@ angular.module("admin.orders").controller "ordersCtrl", ($scope, RequestMonitor,
|
||||
page: page
|
||||
})
|
||||
|
||||
$scope.resetSelected = ->
|
||||
$scope.selected_orders.length = 0
|
||||
$scope.selected = false
|
||||
$scope.select_all = false
|
||||
$scope.checkboxes = {}
|
||||
|
||||
$scope.toggleSelection = (id) ->
|
||||
index = $scope.selected_orders.indexOf(id)
|
||||
|
||||
if index == -1
|
||||
$scope.selected_orders.push(id)
|
||||
else
|
||||
$scope.selected_orders.splice(index, 1)
|
||||
|
||||
$scope.toggleAll = ->
|
||||
$scope.selected_orders.length = 0
|
||||
$scope.orders.forEach (order) ->
|
||||
$scope.checkboxes[order.id] = $scope.select_all
|
||||
$scope.selected_orders.push order.id if $scope.select_all
|
||||
|
||||
$scope.$watch 'sortOptions', (sort) ->
|
||||
if sort && sort.predicate != ""
|
||||
$scope.sorting = sort.predicate + ' desc' if sort.reverse
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
angular.module("admin.orders").directive "invoicesModal", ($modal) ->
|
||||
restrict: 'C'
|
||||
link: (scope, elem, attrs, ctrl) ->
|
||||
elem.on "click", (ev) =>
|
||||
scope.uploadModal = $modal.open(templateUrl: 'admin/modals/bulk_invoice.html', controller: ctrl, scope: scope, windowClass: 'simple-modal')
|
||||
@@ -1 +1 @@
|
||||
angular.module("admin.orders", ['admin.indexUtils', 'ngResource'])
|
||||
angular.module("admin.orders", ['admin.indexUtils', 'ngResource', 'mm.foundation'])
|
||||
|
||||
@@ -2,5 +2,5 @@ angular.module("ofn.admin").directive "imageModal", ($modal, ProductImageService
|
||||
restrict: 'C'
|
||||
link: (scope, elem, attrs, ctrl) ->
|
||||
elem.on "click", (ev) =>
|
||||
scope.uploadModal = $modal.open(templateUrl: 'admin/modals/image_upload.html', controller: ctrl, scope: scope, windowClass: 'product-image-upload')
|
||||
scope.uploadModal = $modal.open(templateUrl: 'admin/modals/image_upload.html', controller: ctrl, scope: scope, windowClass: 'simple-modal')
|
||||
ProductImageService.configure(scope.product)
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
%h4.modal-title
|
||||
= t('js.admin.orders.index.compiling_invoices')
|
||||
|
||||
%p.message{ ng: { show: 'message' } }
|
||||
{{message}}
|
||||
%p.error{ ng: { show: 'error' } }
|
||||
{{error}}
|
||||
|
||||
%img.spinner{ src: "/assets/spinning-circles.svg", ng: { show: "loading" } }
|
||||
%p{ ng: { show: "loading" } }
|
||||
= t('js.admin.orders.index.please_wait')
|
||||
|
||||
%a.button{ target: '_blank', ng: { click: 'showBulkInvoice()', href: '/admin/orders/invoices/{{invoice_id}}', show: "!loading && !error" } }
|
||||
= t('js.admin.orders.index.view_file')
|
||||
|
||||
@@ -17,4 +17,3 @@
|
||||
@import 'variables';
|
||||
@import 'components/*';
|
||||
@import '*';
|
||||
@import 'pages/*';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
@import '../variables';
|
||||
|
||||
.reveal-modal.product-image-upload {
|
||||
.reveal-modal.simple-modal {
|
||||
width: 300px;
|
||||
|
||||
.close-reveal-modal {
|
||||
@@ -103,3 +103,34 @@ table.index td.actions {
|
||||
font-size: 1.2em;
|
||||
}
|
||||
}
|
||||
|
||||
.index-controls {
|
||||
|
||||
button {
|
||||
float: right;
|
||||
|
||||
&:disabled {
|
||||
background-color: $disabled-button;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.simple-modal {
|
||||
text-align: center;
|
||||
|
||||
.modal-title {
|
||||
margin-bottom: 1.5em;
|
||||
}
|
||||
|
||||
.message, .error {
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
.error {
|
||||
color: $warning-red;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,86 +0,0 @@
|
||||
@import typography
|
||||
|
||||
.darkswarm
|
||||
|
||||
navigation
|
||||
display: block
|
||||
background: #f7f7f7
|
||||
|
||||
distributor.details
|
||||
box-sizing: border-box
|
||||
display: block
|
||||
min-height: 150px
|
||||
padding: 30px 0px 20px 0px
|
||||
select
|
||||
width: 200px
|
||||
position: relative
|
||||
img
|
||||
display: block
|
||||
height: 100px
|
||||
width: 100px
|
||||
margin-right: 12px
|
||||
|
||||
location
|
||||
@include headingFont
|
||||
@media all and (max-width: 768px)
|
||||
location, location + small
|
||||
display: block
|
||||
|
||||
#distributor_title h3
|
||||
margin-top: 0
|
||||
@media all and (max-width: 768px)
|
||||
margin-bottom: 8px
|
||||
|
||||
|
||||
ordercycle
|
||||
text-align: right
|
||||
p
|
||||
max-width: 400px
|
||||
h4 i
|
||||
margin-right: 0.3rem
|
||||
@media all and (max-width: 640px)
|
||||
float: left
|
||||
clear: left
|
||||
text-align: left
|
||||
padding: 12px 10px
|
||||
width: 100%
|
||||
margin-top: 10px
|
||||
background: #e5e5e5
|
||||
p
|
||||
max-width: 100%
|
||||
float: right
|
||||
form.custom
|
||||
text-align: right
|
||||
& > strong
|
||||
line-height: 2.5
|
||||
font-size: 1.29em
|
||||
padding-right: 14px
|
||||
@media all and (max-width: 768px)
|
||||
select
|
||||
width: inherit
|
||||
display: inline-block
|
||||
border-width: 1px
|
||||
border-color: #999
|
||||
color: #666
|
||||
font-size: 1em
|
||||
margin-bottom: 0
|
||||
padding: 8px 20px 8px 12px
|
||||
@media all and (max-width: 768px)
|
||||
font-size: 0.875em
|
||||
@media screen and (-webkit-min-device-pixel-ratio:0)
|
||||
font-size: 16px
|
||||
closing
|
||||
@include headingFont
|
||||
@media all and (max-width: 768px)
|
||||
font-size: 1.2em
|
||||
padding-bottom: 10px
|
||||
color: black
|
||||
font-size: 1.5em
|
||||
display: block
|
||||
padding-bottom: 12px
|
||||
span
|
||||
@media all and (max-width: 768px)
|
||||
font-size: 0.875em
|
||||
|
||||
|
||||
|
||||
103
app/assets/stylesheets/darkswarm/_shop-navigation.css.scss
Normal file
103
app/assets/stylesheets/darkswarm/_shop-navigation.css.scss
Normal file
@@ -0,0 +1,103 @@
|
||||
@import "typography";
|
||||
|
||||
.darkswarm navigation {
|
||||
display: block;
|
||||
background: #f7f7f7;
|
||||
|
||||
distributor.details {
|
||||
box-sizing: border-box;
|
||||
display: block;
|
||||
min-height: 150px;
|
||||
padding: 30px 0 20px 0;
|
||||
position: relative;
|
||||
select {
|
||||
width: 200px;
|
||||
}
|
||||
img {
|
||||
display: block;
|
||||
height: 100px;
|
||||
width: 100px;
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
location {
|
||||
@include headingFont;
|
||||
}
|
||||
@media all and (max-width: 768px) {
|
||||
location, location + small {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
#distributor_title h3 {
|
||||
margin-top: 0;
|
||||
@media all and (max-width: 768px) {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
ordercycle {
|
||||
text-align: right;
|
||||
float: right;
|
||||
p {
|
||||
max-width: 400px;
|
||||
}
|
||||
h4 i {
|
||||
margin-right: 0.3rem;
|
||||
}
|
||||
@media all and (max-width: 640px) {
|
||||
float: left;
|
||||
clear: left;
|
||||
text-align: left;
|
||||
padding: 12px 10px;
|
||||
width: 100%;
|
||||
margin-top: 10px;
|
||||
background: #e5e5e5;
|
||||
p {
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
form.custom {
|
||||
text-align: right;
|
||||
& > strong {
|
||||
line-height: 2.5;
|
||||
font-size: 1.29em;
|
||||
padding-right: 14px;
|
||||
}
|
||||
select {
|
||||
width: inherit;
|
||||
display: inline-block;
|
||||
border: 1px #999;
|
||||
color: #666;
|
||||
font-size: 1em;
|
||||
margin-bottom: 0;
|
||||
padding: 8px 20px 8px 12px;
|
||||
@media all and (max-width: 768px) {
|
||||
font-size: 0.875em;
|
||||
}
|
||||
@media screen and (-webkit-min-device-pixel-ratio: 0) {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
closing {
|
||||
@include headingFont;
|
||||
color: black;
|
||||
font-size: 1.5em;
|
||||
display: block;
|
||||
padding-bottom: 12px;
|
||||
@media all and (max-width: 768px) {
|
||||
font-size: 1.2em;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
span {
|
||||
@media all and (max-width: 768px) {
|
||||
font-size: 0.875em;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,86 +0,0 @@
|
||||
@import mixins
|
||||
@import branding
|
||||
@import animations
|
||||
|
||||
.order-summary
|
||||
background-color: #e1f0f5
|
||||
padding: 1em
|
||||
width: 100%
|
||||
border: none
|
||||
color: inherit
|
||||
|
||||
checkout
|
||||
display: block
|
||||
|
||||
@media all and (max-width: 640px)
|
||||
&.row .row
|
||||
margin-left: 0
|
||||
margin-right: 0
|
||||
|
||||
orderdetails
|
||||
.button, table
|
||||
width: 100%
|
||||
@media all and (max-width: 640px)
|
||||
form.edit_order
|
||||
border: 1px solid $disabled-bright
|
||||
margin-bottom: 2rem
|
||||
|
||||
#details, #billing, #shipping, #payment
|
||||
border: 0
|
||||
margin: 1em 0
|
||||
padding: 0
|
||||
.content
|
||||
border: 1px solid #efefef
|
||||
|
||||
h5
|
||||
margin: 0
|
||||
padding: 0.65em
|
||||
background: #f7f7f7
|
||||
|
||||
.label
|
||||
font-size: 1em
|
||||
padding: 0.3rem 0.35rem 0.275rem
|
||||
|
||||
// Logic to turn on & off the alerts for success against each fieldset
|
||||
|
||||
label, label.alert, label.success, &.valid label.alert, &.dirty label.success
|
||||
display: none
|
||||
|
||||
&.dirty label.alert
|
||||
display: inline
|
||||
&.dirty.valid label.alert
|
||||
display: none
|
||||
&.valid label.success
|
||||
display: inline
|
||||
|
||||
h5.dirty
|
||||
background: #f7ccc5
|
||||
h5.valid, h5.dirty.valid
|
||||
background: #bfefd1
|
||||
|
||||
orderdetails table tr th
|
||||
text-align: left
|
||||
|
||||
// Logic to swap out up / down accordion icons
|
||||
//Foundation overrides
|
||||
dd > a
|
||||
@include csstrans
|
||||
background: $disabled-light !important
|
||||
|
||||
dd > a:hover
|
||||
background: $disabled-v-dark !important
|
||||
color: white
|
||||
|
||||
dd
|
||||
span.accordion-up
|
||||
display: none
|
||||
span.accordion-down
|
||||
display: inline
|
||||
&.open
|
||||
span.accordion-up
|
||||
display: inline
|
||||
span.accordion-down
|
||||
display: none
|
||||
|
||||
.error
|
||||
color: #c82020
|
||||
117
app/assets/stylesheets/darkswarm/checkout.css.scss
Normal file
117
app/assets/stylesheets/darkswarm/checkout.css.scss
Normal file
@@ -0,0 +1,117 @@
|
||||
@import "mixins";
|
||||
@import "branding";
|
||||
@import "animations";
|
||||
|
||||
.order-summary {
|
||||
background-color: #e1f0f5;
|
||||
padding: 1em;
|
||||
width: 100%;
|
||||
border: none;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
checkout {
|
||||
display: block;
|
||||
|
||||
@media all and (max-width: 640px) {
|
||||
&.row .row {
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
orderdetails {
|
||||
.button, table {
|
||||
width: 100%;
|
||||
}
|
||||
@media all and (max-width: 640px) {
|
||||
form.edit_order {
|
||||
border: 1px solid $disabled-bright;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#details, #billing, #shipping, #payment {
|
||||
border: 0;
|
||||
margin: 1em 0;
|
||||
padding: 0;
|
||||
.content {
|
||||
border: 1px solid #efefef;
|
||||
}
|
||||
}
|
||||
|
||||
h5 {
|
||||
margin: 0;
|
||||
padding: 0.65em;
|
||||
background: #f7f7f7;
|
||||
|
||||
.label {
|
||||
font-size: 1em;
|
||||
padding: 0.3rem 0.35rem 0.275rem;
|
||||
}
|
||||
|
||||
// Logic to turn on & off the alerts for success against each fieldset
|
||||
|
||||
label, label.alert, label.success, &.valid label.alert, &.dirty label.success {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&.dirty label.alert {
|
||||
display: inline;
|
||||
}
|
||||
&.dirty.valid label.alert {
|
||||
display: none;
|
||||
}
|
||||
&.valid label.success {
|
||||
display: inline;
|
||||
}
|
||||
}
|
||||
|
||||
h5.dirty {
|
||||
background: #f7ccc5;
|
||||
}
|
||||
|
||||
h5.valid, h5.dirty.valid {
|
||||
background: #bfefd1;
|
||||
}
|
||||
|
||||
orderdetails table tr th {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
|
||||
// Logic to swap out up / down accordion icons
|
||||
//Foundation overrides
|
||||
dd > a {
|
||||
@include csstrans;
|
||||
background: $disabled-light !important;
|
||||
}
|
||||
|
||||
dd > a:hover {
|
||||
background: $disabled-v-dark !important;
|
||||
color: white;
|
||||
}
|
||||
|
||||
dd {
|
||||
span.accordion-up {
|
||||
display: none;
|
||||
}
|
||||
span.accordion-down {
|
||||
display: inline;
|
||||
}
|
||||
&.open {
|
||||
span.accordion-up {
|
||||
display: inline;
|
||||
}
|
||||
span.accordion-down {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.error {
|
||||
color: #c82020;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,3 +4,11 @@
|
||||
fieldset {
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.user-form {
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
max-width: 1184px;
|
||||
padding-left: .9375rem;
|
||||
padding-right: .9375rem;
|
||||
}
|
||||
|
||||
31
app/controllers/spree/admin/invoices_controller.rb
Normal file
31
app/controllers/spree/admin/invoices_controller.rb
Normal file
@@ -0,0 +1,31 @@
|
||||
module Spree
|
||||
module Admin
|
||||
class InvoicesController < Spree::Admin::BaseController
|
||||
respond_to :json
|
||||
|
||||
def create
|
||||
invoice_service = BulkInvoiceService.new
|
||||
invoice_service.start_pdf_job(params[:order_ids])
|
||||
|
||||
render json: invoice_service.id, status: :ok
|
||||
end
|
||||
|
||||
def show
|
||||
invoice_id = params[:id]
|
||||
invoice_pdf = BulkInvoiceService.new.filepath(invoice_id)
|
||||
|
||||
send_file(invoice_pdf, type: 'application/pdf', disposition: :inline)
|
||||
end
|
||||
|
||||
def poll
|
||||
invoice_id = params[:invoice_id]
|
||||
|
||||
if BulkInvoiceService.new.invoice_created? invoice_id
|
||||
render json: { created: true }, status: :ok
|
||||
else
|
||||
render json: { created: false }, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -37,8 +37,10 @@ Spree::Admin::OrdersController.class_eval do
|
||||
end
|
||||
|
||||
def invoice
|
||||
template = if Spree::Config.invoice_style2? then "spree/admin/orders/invoice2" else "spree/admin/orders/invoice" end
|
||||
pdf = render_to_string pdf: "invoice-#{@order.number}.pdf", template: template, formats: [:html], encoding: "UTF-8"
|
||||
pdf = render_to_string pdf: "invoice-#{@order.number}.pdf",
|
||||
template: invoice_template,
|
||||
formats: [:html], encoding: "UTF-8"
|
||||
|
||||
Spree::OrderMailer.invoice_email(@order.id, pdf).deliver
|
||||
flash[:success] = t('admin.orders.invoice_email_sent')
|
||||
|
||||
@@ -46,8 +48,7 @@ Spree::Admin::OrdersController.class_eval do
|
||||
end
|
||||
|
||||
def print
|
||||
template = if Spree::Config.invoice_style2? then "spree/admin/orders/invoice2" else "spree/admin/orders/invoice" end
|
||||
render pdf: "invoice-#{@order.number}", template: template, encoding: "UTF-8"
|
||||
render pdf: "invoice-#{@order.number}", template: invoice_template, encoding: "UTF-8"
|
||||
end
|
||||
|
||||
def print_ticket
|
||||
@@ -60,6 +61,10 @@ Spree::Admin::OrdersController.class_eval do
|
||||
|
||||
private
|
||||
|
||||
def invoice_template
|
||||
Spree::Config.invoice_style2? ? "spree/admin/orders/invoice2" : "spree/admin/orders/invoice"
|
||||
end
|
||||
|
||||
def require_distributor_abn
|
||||
unless @order.distributor.abn.present?
|
||||
flash[:error] = t(:must_have_valid_business_number, enterprise_name: @order.distributor.name)
|
||||
|
||||
@@ -208,7 +208,9 @@ class AbilityDecorator
|
||||
# during the order creation process from the admin backend
|
||||
order.distributor.nil? || user.enterprises.include?(order.distributor) || order.order_cycle.andand.coordinated_by?(user)
|
||||
end
|
||||
can [:admin, :bulk_management, :managed], Spree::Order if user.admin? || user.enterprises.any?(&:is_distributor)
|
||||
can [:admin, :bulk_management, :managed, :bulk_invoice], Spree::Order do
|
||||
user.admin? || user.enterprises.any?(&:is_distributor)
|
||||
end
|
||||
can [:admin, :visible], Enterprise
|
||||
can [:admin, :index, :create, :update, :destroy], :line_item
|
||||
can [:admin, :index, :create], Spree::LineItem
|
||||
|
||||
56
app/services/bulk_invoice_service.rb
Normal file
56
app/services/bulk_invoice_service.rb
Normal file
@@ -0,0 +1,56 @@
|
||||
class BulkInvoiceService
|
||||
include WickedPdf::PdfHelper
|
||||
attr_reader :id
|
||||
|
||||
def initialize
|
||||
@id = new_invoice_id
|
||||
end
|
||||
|
||||
def start_pdf_job(order_ids)
|
||||
pdf = CombinePDF.new
|
||||
orders = Spree::Order.where(id: order_ids)
|
||||
|
||||
orders.each do |order|
|
||||
invoice = renderer.render_to_string pdf: "invoice-#{order.number}.pdf",
|
||||
template: invoice_template,
|
||||
formats: [:html], encoding: "UTF-8",
|
||||
locals: { :@order => order }
|
||||
|
||||
pdf << CombinePDF.parse(invoice)
|
||||
end
|
||||
|
||||
pdf.save "#{file_directory}/#{@id}.pdf"
|
||||
end
|
||||
handle_asynchronously :start_pdf_job
|
||||
|
||||
def invoice_created?(invoice_id)
|
||||
File.exist? filepath(invoice_id)
|
||||
end
|
||||
|
||||
def filepath(invoice_id)
|
||||
"#{directory}/#{invoice_id}.pdf"
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def new_invoice_id
|
||||
Time.zone.now.to_i.to_s
|
||||
end
|
||||
|
||||
def directory
|
||||
'tmp/invoices'
|
||||
end
|
||||
|
||||
def renderer
|
||||
ApplicationController.new
|
||||
end
|
||||
|
||||
def invoice_template
|
||||
Spree::Config.invoice_style2? ? "spree/admin/orders/invoice2" : "spree/admin/orders/invoice"
|
||||
end
|
||||
|
||||
def file_directory
|
||||
Dir.mkdir(directory) unless File.exist?(directory)
|
||||
directory
|
||||
end
|
||||
end
|
||||
@@ -2,4 +2,4 @@
|
||||
I18n.default_locale = "#{I18n.default_locale}";
|
||||
I18n.locale = "#{I18n.locale}";
|
||||
I18n.fallbacks = true;
|
||||
moment.lang([I18n.locale, 'en']);
|
||||
moment.locale([I18n.locale, 'en']);
|
||||
|
||||
@@ -16,14 +16,19 @@
|
||||
- content_for :table_filter do
|
||||
= render partial: 'filters'
|
||||
|
||||
.row
|
||||
.row.index-controls{'ng-show' => '!RequestMonitor.loading && orders.length > 0'}
|
||||
= render partial: 'per_page_controls'
|
||||
|
||||
%button.invoices-modal{'ng-controller' => 'bulkInvoiceCtrl', 'ng-click' => 'createBulkInvoice()', 'ng-disabled' => 'selected_orders.length == 0'}
|
||||
= t('.print_invoices')
|
||||
|
||||
%table#listing_orders.index.responsive{width: "100%", 'ng-init' => 'initialise()', 'ng-show' => "!RequestMonitor.loading && orders.length > 0" }
|
||||
%colgroup
|
||||
%col{style: "width: 10%"}
|
||||
%col{style: "width: 3%"}
|
||||
%thead
|
||||
%tr
|
||||
%th
|
||||
%input{type: 'checkbox', 'ng-click' => 'toggleAll()', 'ng-model' => 'select_all'}
|
||||
%th
|
||||
= t(:products_distributor)
|
||||
%th
|
||||
@@ -39,6 +44,8 @@
|
||||
%th.actions
|
||||
%tbody
|
||||
%tr{ng: {repeat: 'order in orders track by $index', class: {even: "'even'", odd: "'odd'"}}, 'ng-class' => "'state-{{order.state}}'"}
|
||||
%td.align-center
|
||||
%input{type: 'checkbox', 'ng-model' => 'checkboxes[order.id]', 'ng-change' => 'toggleSelection(order.id)'}
|
||||
%td.align-center
|
||||
{{order.distributor_name}}
|
||||
%td.align-center
|
||||
|
||||
@@ -30,13 +30,14 @@
|
||||
|
||||
%td{ :align => "right" }
|
||||
%strong= "#{t('.to')}: #{@order.ship_address.full_name}"
|
||||
- if @order.customer.code.present?
|
||||
- if @order.andand.customer.andand.code.present?
|
||||
%br
|
||||
= "#{t('.code')}: #{@order.customer.code}"
|
||||
%br
|
||||
= @order.ship_address.full_address
|
||||
%br
|
||||
= "#{@order.customer.email},"
|
||||
- if @order.andand.customer.andand.email.present?
|
||||
= "#{@order.customer.email},"
|
||||
= "#{@order.bill_address.phone}"
|
||||
|
||||
= render 'spree/admin/orders/invoice_table'
|
||||
|
||||
@@ -45,7 +45,7 @@
|
||||
= t :invoice_billing_address
|
||||
%br
|
||||
%strong= @order.ship_address.full_name
|
||||
- if @order.customer.code.present?
|
||||
- if @order.andand.customer.andand.code.present?
|
||||
%br
|
||||
= "Code: #{@order.customer.code}"
|
||||
%br
|
||||
|
||||
@@ -50,6 +50,6 @@
|
||||
- if @order.order_cycle.andand.pickup_instructions_for(@order.distributor).present?
|
||||
%p
|
||||
%strong
|
||||
= t :email_shipping_collection_time
|
||||
= t :email_shipping_collection_instructions
|
||||
%br
|
||||
#{@order.order_cycle.pickup_instructions_for(@order.distributor)}
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
.darkswarm
|
||||
.row
|
||||
.user-form
|
||||
= render 'form'
|
||||
|
||||
@@ -2601,6 +2601,11 @@ See the %{link} to find out more about %{sitename}'s features and to start using
|
||||
orders:
|
||||
index:
|
||||
per_page: "%{results} per page"
|
||||
view_file: View File
|
||||
compiling_invoices: Compiling Invoices
|
||||
bulk_invoice_created: Bulk Invoice created
|
||||
bulk_invoice_failed: Failed to create Bulk Invoice
|
||||
please_wait: Please wait until the PDF is ready before closing this modal.
|
||||
resend_user_email_confirmation:
|
||||
resend: "Resend"
|
||||
sending: "Resend..."
|
||||
@@ -2733,6 +2738,7 @@ See the %{link} to find out more about %{sitename}'s features and to start using
|
||||
no_orders_found: "No Orders Found"
|
||||
results_found: "%{number} Results found."
|
||||
viewing: "Viewing %{start} to %{end}."
|
||||
print_invoices: "Print Invoices"
|
||||
invoice:
|
||||
issued_on: Issued on
|
||||
tax_invoice: TAX INVOICE
|
||||
|
||||
@@ -74,6 +74,12 @@ Spree::Core::Engine.routes.prepend do
|
||||
get :print, on: :member
|
||||
get :print_ticket, on: :member
|
||||
get :managed, on: :collection
|
||||
|
||||
collection do
|
||||
resources :invoices, only: [:create, :show] do
|
||||
get :poll
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
43
spec/controllers/spree/admin/invoices_controller_spec.rb
Normal file
43
spec/controllers/spree/admin/invoices_controller_spec.rb
Normal file
@@ -0,0 +1,43 @@
|
||||
require 'spec_helper'
|
||||
|
||||
describe Spree::Admin::InvoicesController, type: :controller do
|
||||
let(:order) { create(:order_with_totals_and_distribution) }
|
||||
let(:user) { create(:admin_user) }
|
||||
|
||||
before do
|
||||
allow(controller).to receive(:spree_current_user) { user }
|
||||
end
|
||||
|
||||
describe "#create" do
|
||||
it "enqueues a job to create a bulk invoice and returns the filename" do
|
||||
expect do
|
||||
spree_post :create, order_ids: [order.id]
|
||||
end.to enqueue_job Delayed::PerformableMethod
|
||||
|
||||
expect(Delayed::Job.last.payload_object.method_name).to eq :start_pdf_job_without_delay
|
||||
end
|
||||
end
|
||||
|
||||
describe "#poll" do
|
||||
let(:invoice_id) { '479186263' }
|
||||
|
||||
context "when the file is available" do
|
||||
it "returns true" do
|
||||
allow(File).to receive(:exist?).and_return(true)
|
||||
spree_get :poll, invoice_id: invoice_id
|
||||
|
||||
expect(response.body).to eq({ created: true }.to_json)
|
||||
expect(response.status).to eq 200
|
||||
end
|
||||
end
|
||||
|
||||
context "when the file is not available" do
|
||||
it "returns false" do
|
||||
spree_get :poll, invoice_id: invoice_id
|
||||
|
||||
expect(response.body).to eq({ created: false }.to_json)
|
||||
expect(response.status).to eq 422
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -765,9 +765,9 @@ feature %q{
|
||||
end
|
||||
|
||||
# Shows upload modal
|
||||
expect(page).to have_selector "div.reveal-modal.product-image-upload"
|
||||
expect(page).to have_selector "div.reveal-modal"
|
||||
|
||||
within "div.reveal-modal.product-image-upload" do
|
||||
within "div.reveal-modal" do
|
||||
# Shows preview of current image
|
||||
expect(page).to have_css "img.preview"
|
||||
|
||||
@@ -779,7 +779,7 @@ feature %q{
|
||||
expect(page).to have_no_css "img.spinner", visible: true
|
||||
end
|
||||
|
||||
expect(page).to have_no_selector "div.reveal-modal.product-image-upload"
|
||||
expect(page).to have_no_selector "div.reveal-modal"
|
||||
|
||||
within "table#listing_products tr#p_#{product.id}" do
|
||||
# New thumbnail is shown in image column
|
||||
@@ -789,7 +789,7 @@ feature %q{
|
||||
page.find("a.image-modal").click
|
||||
end
|
||||
|
||||
expect(page).to have_selector "div.reveal-modal.product-image-upload"
|
||||
expect(page).to have_selector "div.reveal-modal"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -115,13 +115,15 @@ feature "Using embedded shopfront functionality", js: true do
|
||||
end
|
||||
|
||||
def login_with_modal
|
||||
expect(page).to have_selector 'div.login-modal', visible: true
|
||||
page.has_selector? 'div.login-modal', visible: true
|
||||
|
||||
within 'div.login-modal' do
|
||||
fill_in "Email", with: user.email
|
||||
fill_in "Password", with: user.password
|
||||
find('input[type="submit"]').click
|
||||
end
|
||||
|
||||
page.has_no_selector? 'div.login-modal', visible: true
|
||||
end
|
||||
|
||||
def logout_via_navigation
|
||||
|
||||
@@ -197,6 +197,6 @@ xdescribe ProxyOrder, type: :model do
|
||||
# We still need to use be_within, because the Database timestamp is not as
|
||||
# accurate as the Rails timestamp. If we use `eq`, we have differing nano
|
||||
# seconds.
|
||||
expect(subject.reload.canceled_at).to be_within(1.second).of Time.zone.now
|
||||
expect(subject.reload.canceled_at).to be_within(2.second).of Time.zone.now
|
||||
end
|
||||
end
|
||||
|
||||
@@ -514,5 +514,89 @@ module Spree
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "permissions for variant overrides" do
|
||||
let!(:distributor) { create(:distributor_enterprise) }
|
||||
let!(:producer) { create(:supplier_enterprise) }
|
||||
let!(:product) { create(:product, supplier: producer) }
|
||||
let!(:variant) { create(:variant, product: product) }
|
||||
let!(:variant_override) { create(:variant_override, hub: distributor, variant: variant) }
|
||||
|
||||
subject { user }
|
||||
|
||||
let(:manage_actions) { [:admin, :index, :read, :update, :bulk_update, :bulk_reset] }
|
||||
|
||||
describe "when admin" do
|
||||
let(:user) { create(:admin_user) }
|
||||
|
||||
it "should have permission" do
|
||||
is_expected.to have_ability(manage_actions, for: variant_override)
|
||||
end
|
||||
end
|
||||
|
||||
describe "when user of the producer" do
|
||||
let(:user) { producer.owner }
|
||||
|
||||
it "should not have permission" do
|
||||
is_expected.not_to have_ability(manage_actions, for: variant_override)
|
||||
end
|
||||
end
|
||||
|
||||
describe "when user of the distributor" do
|
||||
let(:user) { distributor.owner }
|
||||
|
||||
it "should not have permission" do
|
||||
is_expected.not_to have_ability(manage_actions, for: variant_override)
|
||||
end
|
||||
end
|
||||
|
||||
describe "when user of the distributor which is also the producer" do
|
||||
let(:user) { distributor.owner }
|
||||
let!(:distributor) { create(:distributor_enterprise, is_primary_producer: true, sells: "any") }
|
||||
let!(:producer) { distributor }
|
||||
|
||||
it "should have permission" do
|
||||
is_expected.to have_ability(manage_actions, for: variant_override)
|
||||
end
|
||||
end
|
||||
|
||||
describe "when owner of the distributor with add_to_order_cycle permission to the producer" do
|
||||
let!(:unauthorized_enterprise) do
|
||||
create(:enterprise, sells: "any").tap do |record|
|
||||
create(:enterprise_relationship, parent: producer, child: record, permissions_list: [:add_to_order_cycle])
|
||||
end
|
||||
end
|
||||
let(:user) { unauthorized_enterprise.owner }
|
||||
|
||||
it "should not have permission" do
|
||||
is_expected.not_to have_ability(manage_actions, for: variant_override)
|
||||
end
|
||||
end
|
||||
|
||||
describe "when owner of the enterprise with create_variant_overrides permission to the producer" do
|
||||
let!(:authorized_enterprise) do
|
||||
create(:enterprise, sells: "any").tap do |record|
|
||||
create(:enterprise_relationship, parent: producer, child: record, permissions_list: [:create_variant_overrides])
|
||||
end
|
||||
end
|
||||
let(:user) { authorized_enterprise.owner }
|
||||
|
||||
it "should not have permission" do
|
||||
is_expected.not_to have_ability(manage_actions, for: variant_override)
|
||||
end
|
||||
|
||||
describe "when the enterprise is not a distributor" do
|
||||
let!(:authorized_enterprise) do
|
||||
create(:enterprise, sells: "none").tap do |record|
|
||||
create(:enterprise_relationship, parent: producer, child: record, permissions_list: [:create_variant_overrides])
|
||||
end
|
||||
end
|
||||
|
||||
it "should not have permission" do
|
||||
is_expected.not_to have_ability(manage_actions, for: variant_override)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
40
spec/services/bulk_invoice_service_spec.rb
Normal file
40
spec/services/bulk_invoice_service_spec.rb
Normal file
@@ -0,0 +1,40 @@
|
||||
require 'spec_helper'
|
||||
|
||||
describe BulkInvoiceService do
|
||||
let(:service) { BulkInvoiceService.new }
|
||||
|
||||
describe "#start_pdf_job" do
|
||||
it "starts a background process to create a pdf with multiple invoices" do
|
||||
expect do
|
||||
service.start_pdf_job [1, 2]
|
||||
end.to enqueue_job Delayed::PerformableMethod
|
||||
|
||||
expect(Delayed::Job.last.payload_object.method_name).to eq :start_pdf_job_without_delay
|
||||
end
|
||||
end
|
||||
|
||||
describe "#invoice_created?" do
|
||||
context "when the invoice has been created" do
|
||||
it "returns true" do
|
||||
allow(File).to receive(:exist?).and_return(true)
|
||||
|
||||
created = service.invoice_created? '45891723'
|
||||
expect(created).to be_truthy
|
||||
end
|
||||
end
|
||||
|
||||
context "when the invoice has not been created" do
|
||||
it "returns false" do
|
||||
created = service.invoice_created? '1234567'
|
||||
expect(created).to_not be_truthy
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "#filepath" do
|
||||
it "returns the filepath of a given invoice" do
|
||||
filepath = service.filepath '1234567'
|
||||
expect(filepath).to eq 'tmp/invoices/1234567.pdf'
|
||||
end
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user