diff --git a/CHANGELOG.md b/CHANGELOG.md index 33c367e39..d0322400d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ # Changelog Fab Manager +- Ability to configure and export the accounting data to the ACD accounting software - Fix a bug: no user can be created after the last member was deleted - Fix a bug: unable to generate a refund (Avoir) - Fix a bug: a newly generated refund is displayed as broken (unchained record) even if it is correctly chained @@ -9,6 +10,7 @@ - Fix some security issues: updated sidekiq to 5.2.7 to fix XSS and CRSF issues - Removed dependency to jQuery UI - Updated angular-xeditable, to remove dependency to jquery 1.11.1 +- [TODO DEPLOY] `rake db:migrate` ## v4.0.2 2019 July 10 diff --git a/app/controllers/api/accounting_exports_controller.rb b/app/controllers/api/accounting_exports_controller.rb new file mode 100644 index 000000000..9efa1a321 --- /dev/null +++ b/app/controllers/api/accounting_exports_controller.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +# API Controller for exporting accounting data to external accounting softwares +class API::AccountingExportsController < API::ApiController + + before_action :authenticate_user! + + def export + authorize :export + + export = Export.where(category: 'accounting', export_type: 'accounting-software') + .where('created_at > ?', Invoice.maximum('updated_at')) + .last + if export.nil? || !FileTest.exist?(export.file) + @export = Export.new( + category: 'accounting', + export_type: 'accounting-software', + user: current_user, + extension: params[:extension], + query: params[:query], + key: params[:separator] + ) + if @export.save + render json: { export_id: @export.id }, status: :ok + else + render json: @export.errors, status: :unprocessable_entity + end + else + send_file File.join(Rails.root, export.file), + type: 'text/csv', + disposition: 'attachment' + end + end +end diff --git a/app/controllers/api/exports_controller.rb b/app/controllers/api/exports_controller.rb index a785562e9..e02a6e7b4 100644 --- a/app/controllers/api/exports_controller.rb +++ b/app/controllers/api/exports_controller.rb @@ -8,10 +8,17 @@ class API::ExportsController < API::ApiController def download authorize @export + mime_type = if @export.extension == 'xlsx' + 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' + elsif @export.extension == 'csv' + 'text/csv' + else + 'application/octet-stream' + end if FileTest.exist?(@export.file) send_file File.join(Rails.root, @export.file), - type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + type: mime_type, disposition: 'attachment' else render text: I18n.t('errors.messages.export_not_found'), status: :not_found @@ -21,28 +28,8 @@ class API::ExportsController < API::ApiController def status authorize Export - export = Export.where(category: params[:category], export_type: params[:type], query: params[:query], key: params[:key]) - - if params[:category] == 'users' - case params[:type] - when 'subscriptions' - export = export.where('created_at > ?', Subscription.maximum('updated_at')) - when 'reservations' - export = export.where('created_at > ?', Reservation.maximum('updated_at')) - when 'members' - export = export.where('created_at > ?', User.with_role(:member).maximum('updated_at')) - else - raise ArgumentError, "Unknown export users/#{params[:type]}" - end - elsif params[:category] == 'availabilities' - case params[:type] - when 'index' - export = export.where('created_at > ?', [Availability.maximum('updated_at'), Reservation.maximum('updated_at')].max) - else - raise ArgumentError, "Unknown type availabilities/#{params[:type]}" - end - end - export = export.last + exports = Export.where(category: params[:category], export_type: params[:type], query: params[:query], key: params[:key]) + export = retrieve_last_export(exports, params[:category], params[:type]) if export.nil? || !FileTest.exist?(export.file) render json: { exists: false, id: nil }, status: :ok @@ -53,6 +40,39 @@ class API::ExportsController < API::ApiController private + def retrieve_last_export(export, category, type) + case category + when 'users' + case type + when 'subscriptions' + export = export.where('created_at > ?', Subscription.maximum('updated_at')) + when 'reservations' + export = export.where('created_at > ?', Reservation.maximum('updated_at')) + when 'members' + export = export.where('created_at > ?', User.with_role(:member).maximum('updated_at')) + else + raise ArgumentError, "Unknown export users/#{type}" + end + when 'availabilities' + case type + when 'index' + export = export.where('created_at > ?', [Availability.maximum('updated_at'), Reservation.maximum('updated_at')].max) + else + raise ArgumentError, "Unknown type availabilities/#{type}" + end + when 'accounting' + case type + when 'accounting-software' + export = export.where('created_at > ?', Invoice.maximum('updated_at')) + else + raise ArgumentError, "Unknown type accounting/#{type}" + end + else + raise ArgumentError, "Unknown category #{category}" + end + export.last + end + def set_export @export = Export.find(params[:id]) end diff --git a/app/models/export.rb b/app/models/export.rb index 32ab8ed68..74056d3a9 100644 --- a/app/models/export.rb +++ b/app/models/export.rb @@ -21,7 +21,7 @@ class Export < ActiveRecord::Base end def filename - "#{export_type}-#{id}_#{created_at.strftime('%d%m%Y')}.xlsx" + "#{export_type}-#{id}_#{created_at.strftime('%d%m%Y')}.#{extension}" end private @@ -34,6 +34,8 @@ class Export < ActiveRecord::Base UsersExportWorker.perform_async(id) when 'availabilities' AvailabilitiesExportWorker.perform_async(id) + when 'accounting' + AccountingExportWorker.perform_async(id) else raise NoMethodError, "Unknown export service for #{category}/#{export_type}" end diff --git a/app/policies/accounting_exports_policy.rb b/app/policies/accounting_exports_policy.rb new file mode 100644 index 000000000..378dbc2c7 --- /dev/null +++ b/app/policies/accounting_exports_policy.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +# Check the access policies for API::AccountingExportsController +class AccountingExportsPolicy < ApplicationPolicy + def export? + user.admin? + end +end diff --git a/app/workers/accounting_export_worker.rb b/app/workers/accounting_export_worker.rb new file mode 100644 index 000000000..306698398 --- /dev/null +++ b/app/workers/accounting_export_worker.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +# Asynchronously export the accounting data (Invoices & Avoirs) to an external accounting software +class AccountingExportWorker + include Sidekiq::Worker + + def perform(export_id) + export = Export.find(export_id) + + raise SecurityError, 'Not allowed to export' unless export.user.admin? + + data = JSON.parse(export.query) + service = AccountingExportService.new(export.file, data['columns'], data['encoding'], export.extension, export.key) + + service.export(data['start_date'], data['end_date']) + + NotificationCenter.call type: :notify_admin_export_complete, + receiver: export.user, + attached_object: export + end +end diff --git a/config/routes.rb b/config/routes.rb index e036624e5..2193dd9cc 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -135,6 +135,8 @@ Rails.application.routes.draw do get 'last_closing_end', on: :collection get 'archive', action: 'download_archive', on: :member end + # export accounting data to csv or equivalent + post 'accounting/export' => 'accounting_exports#export' # i18n # regex allows using dots in URL for 'state' diff --git a/db/migrate/20190730085826_add_extension_to_export.rb b/db/migrate/20190730085826_add_extension_to_export.rb new file mode 100644 index 000000000..40e15a140 --- /dev/null +++ b/db/migrate/20190730085826_add_extension_to_export.rb @@ -0,0 +1,5 @@ +class AddExtensionToExport < ActiveRecord::Migration + def change + add_column :exports, :extension, :string, default: 'xlsx' + end +end diff --git a/db/schema.rb b/db/schema.rb index f1baff8ba..227076c17 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,12 +11,12 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20190606074801) do +ActiveRecord::Schema.define(version: 20190730085826) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" - enable_extension "unaccent" enable_extension "pg_trgm" + enable_extension "unaccent" create_table "abuses", force: :cascade do |t| t.integer "signaled_id" @@ -202,10 +202,11 @@ ActiveRecord::Schema.define(version: 20190606074801) do t.string "category" t.string "export_type" t.string "query" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false t.integer "user_id" t.string "key" + t.string "extension", default: "xlsx" end add_index "exports", ["user_id"], name: "index_exports_on_user_id", using: :btree