diff --git a/Gemfile b/Gemfile index 5918879945..2e074cd794 100644 --- a/Gemfile +++ b/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' diff --git a/Gemfile.lock b/Gemfile.lock index dff286d0fb..88389705eb 100644 --- a/Gemfile.lock +++ b/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) diff --git a/app/assets/javascripts/admin/orders/controllers/bulk_invoice_controller.js.coffee b/app/assets/javascripts/admin/orders/controllers/bulk_invoice_controller.js.coffee new file mode 100644 index 0000000000..0442564e2e --- /dev/null +++ b/app/assets/javascripts/admin/orders/controllers/bulk_invoice_controller.js.coffee @@ -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() + diff --git a/app/assets/javascripts/admin/orders/controllers/orders_controller.js.coffee b/app/assets/javascripts/admin/orders/controllers/orders_controller.js.coffee index 8501caf6fd..4151b99c9e 100644 --- a/app/assets/javascripts/admin/orders/controllers/orders_controller.js.coffee +++ b/app/assets/javascripts/admin/orders/controllers/orders_controller.js.coffee @@ -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 diff --git a/app/assets/javascripts/admin/orders/directives/invoices_modal.js.coffee b/app/assets/javascripts/admin/orders/directives/invoices_modal.js.coffee new file mode 100644 index 0000000000..0f2eabe778 --- /dev/null +++ b/app/assets/javascripts/admin/orders/directives/invoices_modal.js.coffee @@ -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') diff --git a/app/assets/javascripts/admin/orders/orders.js.coffee b/app/assets/javascripts/admin/orders/orders.js.coffee index 4e1ec754e3..1a8b267d30 100644 --- a/app/assets/javascripts/admin/orders/orders.js.coffee +++ b/app/assets/javascripts/admin/orders/orders.js.coffee @@ -1 +1 @@ -angular.module("admin.orders", ['admin.indexUtils', 'ngResource']) +angular.module("admin.orders", ['admin.indexUtils', 'ngResource', 'mm.foundation']) diff --git a/app/assets/javascripts/admin/products/directives/image_modal.js.coffee b/app/assets/javascripts/admin/products/directives/image_modal.js.coffee index 526c14bf2a..69ca1731fd 100644 --- a/app/assets/javascripts/admin/products/directives/image_modal.js.coffee +++ b/app/assets/javascripts/admin/products/directives/image_modal.js.coffee @@ -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) diff --git a/app/assets/javascripts/templates/admin/modals/bulk_invoice.html.haml b/app/assets/javascripts/templates/admin/modals/bulk_invoice.html.haml new file mode 100644 index 0000000000..16ddb39508 --- /dev/null +++ b/app/assets/javascripts/templates/admin/modals/bulk_invoice.html.haml @@ -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') + diff --git a/app/assets/stylesheets/admin/all.scss b/app/assets/stylesheets/admin/all.scss index 637f0b5d2c..4a16154bf4 100644 --- a/app/assets/stylesheets/admin/all.scss +++ b/app/assets/stylesheets/admin/all.scss @@ -17,4 +17,3 @@ @import 'variables'; @import 'components/*'; @import '*'; -@import 'pages/*'; diff --git a/app/assets/stylesheets/admin/pages/product_image_upload_modal.css.scss b/app/assets/stylesheets/admin/components/simple_modal.css.scss similarity index 87% rename from app/assets/stylesheets/admin/pages/product_image_upload_modal.css.scss rename to app/assets/stylesheets/admin/components/simple_modal.css.scss index 4dffade73e..453fe6da4f 100644 --- a/app/assets/stylesheets/admin/pages/product_image_upload_modal.css.scss +++ b/app/assets/stylesheets/admin/components/simple_modal.css.scss @@ -1,6 +1,6 @@ @import '../variables'; -.reveal-modal.product-image-upload { +.reveal-modal.simple-modal { width: 300px; .close-reveal-modal { diff --git a/app/assets/stylesheets/admin/orders.css.scss b/app/assets/stylesheets/admin/orders.css.scss index 0955d9fd46..4858c0685a 100644 --- a/app/assets/stylesheets/admin/orders.css.scss +++ b/app/assets/stylesheets/admin/orders.css.scss @@ -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; + } +} diff --git a/app/assets/stylesheets/darkswarm/_shop-navigation.css.sass b/app/assets/stylesheets/darkswarm/_shop-navigation.css.sass deleted file mode 100644 index 288450b015..0000000000 --- a/app/assets/stylesheets/darkswarm/_shop-navigation.css.sass +++ /dev/null @@ -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 - - - diff --git a/app/assets/stylesheets/darkswarm/_shop-navigation.css.scss b/app/assets/stylesheets/darkswarm/_shop-navigation.css.scss new file mode 100644 index 0000000000..bd0580958f --- /dev/null +++ b/app/assets/stylesheets/darkswarm/_shop-navigation.css.scss @@ -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; + } + } + } + } + } +} diff --git a/app/assets/stylesheets/darkswarm/checkout.css.sass b/app/assets/stylesheets/darkswarm/checkout.css.sass deleted file mode 100644 index ff18863259..0000000000 --- a/app/assets/stylesheets/darkswarm/checkout.css.sass +++ /dev/null @@ -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 diff --git a/app/assets/stylesheets/darkswarm/checkout.css.scss b/app/assets/stylesheets/darkswarm/checkout.css.scss new file mode 100644 index 0000000000..458fd24d5e --- /dev/null +++ b/app/assets/stylesheets/darkswarm/checkout.css.scss @@ -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; + } +} + diff --git a/app/assets/stylesheets/darkswarm/forms.css.scss b/app/assets/stylesheets/darkswarm/forms.css.scss index 9ca7f6f391..b44b32220f 100644 --- a/app/assets/stylesheets/darkswarm/forms.css.scss +++ b/app/assets/stylesheets/darkswarm/forms.css.scss @@ -4,3 +4,11 @@ fieldset { border: 0; } + +.user-form { + margin-left: auto; + margin-right: auto; + max-width: 1184px; + padding-left: .9375rem; + padding-right: .9375rem; +} diff --git a/app/controllers/spree/admin/invoices_controller.rb b/app/controllers/spree/admin/invoices_controller.rb new file mode 100644 index 0000000000..230d01322b --- /dev/null +++ b/app/controllers/spree/admin/invoices_controller.rb @@ -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 diff --git a/app/controllers/spree/admin/orders_controller_decorator.rb b/app/controllers/spree/admin/orders_controller_decorator.rb index f7e1b56929..84e66451ba 100644 --- a/app/controllers/spree/admin/orders_controller_decorator.rb +++ b/app/controllers/spree/admin/orders_controller_decorator.rb @@ -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) diff --git a/app/models/spree/ability_decorator.rb b/app/models/spree/ability_decorator.rb index bd38794b93..45f3dfe157 100644 --- a/app/models/spree/ability_decorator.rb +++ b/app/models/spree/ability_decorator.rb @@ -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 diff --git a/app/services/bulk_invoice_service.rb b/app/services/bulk_invoice_service.rb new file mode 100644 index 0000000000..397020635e --- /dev/null +++ b/app/services/bulk_invoice_service.rb @@ -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 diff --git a/app/views/layouts/_i18n_script.html.haml b/app/views/layouts/_i18n_script.html.haml index 40884082ac..70cfafba83 100644 --- a/app/views/layouts/_i18n_script.html.haml +++ b/app/views/layouts/_i18n_script.html.haml @@ -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']); diff --git a/app/views/spree/admin/orders/index.html.haml b/app/views/spree/admin/orders/index.html.haml index 8342df101f..1f3a96de34 100644 --- a/app/views/spree/admin/orders/index.html.haml +++ b/app/views/spree/admin/orders/index.html.haml @@ -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 diff --git a/app/views/spree/admin/orders/invoice.html.haml b/app/views/spree/admin/orders/invoice.html.haml index 0982ccc21f..982ef43f59 100644 --- a/app/views/spree/admin/orders/invoice.html.haml +++ b/app/views/spree/admin/orders/invoice.html.haml @@ -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' diff --git a/app/views/spree/admin/orders/invoice2.html.haml b/app/views/spree/admin/orders/invoice2.html.haml index 89f28ab308..837b2be21a 100644 --- a/app/views/spree/admin/orders/invoice2.html.haml +++ b/app/views/spree/admin/orders/invoice2.html.haml @@ -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 diff --git a/app/views/spree/order_mailer/_shipping.html.haml b/app/views/spree/order_mailer/_shipping.html.haml index 195ae77465..a020f2774f 100644 --- a/app/views/spree/order_mailer/_shipping.html.haml +++ b/app/views/spree/order_mailer/_shipping.html.haml @@ -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)} diff --git a/app/views/spree/users/edit.html.haml b/app/views/spree/users/edit.html.haml index 29e600582c..2c586290e3 100644 --- a/app/views/spree/users/edit.html.haml +++ b/app/views/spree/users/edit.html.haml @@ -1,3 +1,3 @@ .darkswarm - .row + .user-form = render 'form' diff --git a/config/locales/en.yml b/config/locales/en.yml index eb14203cf8..f91e1fd3ec 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -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 diff --git a/config/routes/spree.rb b/config/routes/spree.rb index a203ab0fb0..2e9dc0b5c8 100644 --- a/config/routes/spree.rb +++ b/config/routes/spree.rb @@ -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 diff --git a/spec/controllers/spree/admin/invoices_controller_spec.rb b/spec/controllers/spree/admin/invoices_controller_spec.rb new file mode 100644 index 0000000000..1eff65ba2c --- /dev/null +++ b/spec/controllers/spree/admin/invoices_controller_spec.rb @@ -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 diff --git a/spec/features/admin/bulk_product_update_spec.rb b/spec/features/admin/bulk_product_update_spec.rb index 604d9276a8..d8dfd95238 100644 --- a/spec/features/admin/bulk_product_update_spec.rb +++ b/spec/features/admin/bulk_product_update_spec.rb @@ -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 diff --git a/spec/features/consumer/shopping/embedded_shopfronts_spec.rb b/spec/features/consumer/shopping/embedded_shopfronts_spec.rb index 5a97040e32..4a70dbf672 100644 --- a/spec/features/consumer/shopping/embedded_shopfronts_spec.rb +++ b/spec/features/consumer/shopping/embedded_shopfronts_spec.rb @@ -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 diff --git a/spec/models/proxy_order_spec.rb b/spec/models/proxy_order_spec.rb index 22131da7e7..55687c264e 100644 --- a/spec/models/proxy_order_spec.rb +++ b/spec/models/proxy_order_spec.rb @@ -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 diff --git a/spec/models/spree/ability_spec.rb b/spec/models/spree/ability_spec.rb index dcd290e195..ab9946c0ff 100644 --- a/spec/models/spree/ability_spec.rb +++ b/spec/models/spree/ability_spec.rb @@ -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 diff --git a/spec/services/bulk_invoice_service_spec.rb b/spec/services/bulk_invoice_service_spec.rb new file mode 100644 index 0000000000..4611a46846 --- /dev/null +++ b/spec/services/bulk_invoice_service_spec.rb @@ -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