1
0
mirror of https://github.com/LaCasemate/fab-manager.git synced 2025-01-17 06:52:27 +01:00

Merge branch 'async-archive' into dev

This commit is contained in:
Sylvain 2019-04-04 11:37:33 +02:00
commit 1c6d7c266e
20 changed files with 239 additions and 152 deletions

View File

@ -1,67 +1,70 @@
# frozen_string_literal: true
# Various helpers methods
module ApplicationHelper
include Twitter::Autolink
require 'message_format'
include Twitter::Autolink
require 'message_format'
## machine/spaces availabilities are divided in multiple slots of 60 minutes
SLOT_DURATION ||= 60
## machine/spaces availabilities are divided in multiple slots of 60 minutes
SLOT_DURATION ||= 60
##
# Verify if the provided attribute is in the provided attributes array, whatever it exists or not
# @param attributes {Array|nil}
# @param attribute {String}
##
def attribute_requested?(attributes, attribute)
attributes.try(:include?, attribute)
end
##
# Verify if the provided attribute is in the provided attributes array, whatever it exists or not
# @param attributes {Array|nil}
# @param attribute {String}
##
def attribute_requested?(attributes, attribute)
attributes.try(:include?, attribute)
end
def bootstrap_class_for flash_type
{ flash: 'alert-success', alert: 'alert-danger', notice: 'alert-info' }[flash_type.to_sym] || flash_type.to_s
end
def bootstrap_class_for flash_type
{ flash: 'alert-success', alert: 'alert-danger', notice: 'alert-info' }[flash_type.to_sym] || flash_type.to_s
end
def flash_messages(opts = {})
flash.each do |msg_type, message|
concat(content_tag(:div, message, class: "flash-message alert #{bootstrap_class_for(msg_type)} fade in") do
concat content_tag(:button, 'x', class: 'close', data: { dismiss: 'alert' })
concat message
end)
end
nil
end
def flash_messages(_opts = {})
flash.each do |msg_type, message|
concat(content_tag(:div, message, class: "flash-message alert #{bootstrap_class_for(msg_type)} fade in") do
concat content_tag(:button, 'x', class: 'close', data: { dismiss: 'alert' })
concat message
end)
end
nil
end
def print_slot(starting, ending)
"#{starting.strftime('%H:%M')} - #{ending.strftime('%H:%M')}"
end
def print_slot(starting, ending)
"#{starting.strftime('%H:%M')} - #{ending.strftime('%H:%M')}"
end
def class_exists?(class_name)
klass = Module.const_get(class_name)
return klass.is_a?(Class)
rescue NameError
return false
end
def class_exists?(class_name)
klass = Module.const_get(class_name)
klass.is_a?(Class)
rescue NameError
false
end
##
# Allow to treat a rails i18n key as a MessageFormat interpolated pattern. Used in ruby views (API/mails)
# @param key {String} Ruby-on-Rails I18n key (from config/locales/xx.yml)
# @param interpolations {Hash} list of variables to interpolate, following ICU MessageFormat syntax
##
def _t(key, interpolations)
message = MessageFormat.new(I18n.t(scope_key_by_partial(key)), I18n.locale.to_s)
text = message.format(interpolations)
if html_safe_translation_key?(key)
text.html_safe
else
text
end
end
##
# Allow to treat a rails i18n key as a MessageFormat interpolated pattern. Used in ruby views (API/mails)
# @param key {String} Ruby-on-Rails I18n key (from config/locales/xx.yml)
# @param interpolations {Hash} list of variables to interpolate, following ICU MessageFormat syntax
##
def _t(key, interpolations)
message = MessageFormat.new(I18n.t(scope_key_by_partial(key)), I18n.locale.to_s)
text = message.format(interpolations)
if html_safe_translation_key?(key)
text.html_safe
else
text
end
end
def bool_to_sym(bool)
if (bool) then return :true else return :false end
end
def bool_to_sym(bool)
bool ? :true : :false # rubocop:disable Lint/BooleanSymbol
end
def amount_to_f(amount)
amount / 100.00
end
def amount_to_f(amount)
amount / 100.00
end
##
# Retrieve an item in the given array of items
@ -69,46 +72,45 @@ module ApplicationHelper
# this can be overridden by passing a third parameter to specify the
# property to match
##
def get_item(array, id, key = nil)
array.each do |i|
if key.nil?
return i if i.id == id
else
return i if i[key] == id
end
end
nil
end
def get_item(array, id, key = nil)
array.each do |i|
if key.nil?
return i if i.id == id
elsif i[key] == id
return i
end
end
nil
end
##
# Apply a correction for a future DateTime due to change in Daylight Saving Time (DST) period
# @param reference {ActiveSupport::TimeWithZone}
# @param datetime {DateTime}
# Inspired by https://stackoverflow.com/a/12065605
##
def dst_correction(reference, datetime)
res = datetime.in_time_zone(reference.time_zone.tzinfo.name)
res = res - 1.hour if res.dst? && !reference.dst?
res = res + 1.hour if reference.dst? && !res.dst?
res
end
##
# Apply a correction for a future DateTime due to change in Daylight Saving Time (DST) period
# @param reference {ActiveSupport::TimeWithZone}
# @param datetime {DateTime}
# Inspired by https://stackoverflow.com/a/12065605
##
def dst_correction(reference, datetime)
res = datetime.in_time_zone(reference.time_zone.tzinfo.name)
res -= 1.hour if res.dst? && !reference.dst?
res += 1.hour if reference.dst? && !res.dst?
res
end
private
## inspired by gems/actionview-4.2.5/lib/action_view/helpers/translation_helper.rb
def scope_key_by_partial(key)
if key.to_s.first == "."
if @virtual_path
@virtual_path.gsub(%r{/_?}, ".") + key.to_s
else
raise "Cannot use t(#{key.inspect}) shortcut because path is not available"
end
else
key
end
end
private
def html_safe_translation_key?(key)
key.to_s =~ /(\b|_|\.)html$/
end
## inspired by gems/actionview-4.2.5/lib/action_view/helpers/translation_helper.rb
def scope_key_by_partial(key)
if key.to_s.first == '.'
raise "Cannot use t(#{key.inspect}) shortcut because path is not available" unless @virtual_path
@virtual_path.gsub(%r{/_?}, '.') + key.to_s
else
key
end
end
def html_safe_translation_key?(key)
key.to_s =~ /(\b|_|\.)html$/
end
end

View File

@ -1,3 +1,6 @@
# frozen_string_literal: true
# Helpers methods about calendar availabilities
module AvailabilityHelper
MACHINE_COLOR = '#e4cd78'
TRAINING_COLOR = '#bd7ae9'
@ -9,14 +12,14 @@ module AvailabilityHelper
def availability_border_color(availability)
case availability.available_type
when 'machines'
MACHINE_COLOR
when 'training'
TRAINING_COLOR
when 'space'
SPACE_COLOR
else
EVENT_COLOR
when 'machines'
MACHINE_COLOR
when 'training'
TRAINING_COLOR
when 'space'
SPACE_COLOR
else
EVENT_COLOR
end
end
@ -45,14 +48,14 @@ module AvailabilityHelper
IS_COMPLETED
else
case availability.available_type
when 'training'
TRAINING_COLOR
when 'event'
EVENT_COLOR
when 'space'
SPACE_COLOR
else
'#000'
when 'training'
TRAINING_COLOR
when 'event'
EVENT_COLOR
when 'space'
SPACE_COLOR
else
'#000'
end
end
end

View File

@ -1,3 +1,6 @@
# frozen_string_literal: true
# Helpers methods about uploading files
module UploadHelper
def delete_empty_dirs

View File

@ -10,7 +10,7 @@ class AccountingPeriod < ActiveRecord::Base
before_destroy { false }
before_update { false }
before_create :compute_totals
after_create :archive_closed_data
after_commit :archive_closed_data, on: [:create]
validates :start_at, :end_at, :closed_at, :closed_by, presence: true
validates_with DateRangeValidator
@ -68,6 +68,10 @@ class AccountingPeriod < ActiveRecord::Base
end
end
def previous_period
AccountingPeriod.where('closed_at < ?', closed_at).order(closed_at: :desc).limit(1).last
end
private
def vat_history
@ -81,47 +85,8 @@ class AccountingPeriod < ActiveRecord::Base
key_dates.sort_by { |k| k[:date] }
end
def to_json_archive(invoices, previous_file, last_checksum)
code_checksum = Checksum.code
ApplicationController.new.view_context.render(
partial: 'archive/accounting',
locals: {
invoices: invoices_with_vat(invoices),
period_total: period_total,
perpetual_total: perpetual_total,
period_footprint: footprint,
code_checksum: code_checksum,
last_archive_checksum: last_checksum,
previous_file: previous_file,
software_version: Version.current,
date: Time.now.iso8601
},
formats: [:json],
handlers: [:jbuilder]
)
end
def previous_period
AccountingPeriod.where('closed_at < ?', closed_at).order(closed_at: :desc).limit(1).last
end
def archive_closed_data
data = invoices.includes(:invoice_items).order(id: :asc)
previous_file = previous_period&.archive_file
last_archive_checksum = previous_file ? Checksum.file(previous_file) : nil
json_data = to_json_archive(data, previous_file, last_archive_checksum)
current_archive_checksum = Checksum.text(json_data)
date = DateTime.iso8601
chained = Checksum.text("#{current_archive_checksum}#{last_archive_checksum}#{date}")
Zip::OutputStream.open(archive_file) do |io|
io.put_next_entry(archive_json_file)
io.write(json_data)
io.put_next_entry('checksum.sha256')
io.write("#{current_archive_checksum}\t#{archive_json_file}")
io.put_next_entry('chained.sha256')
io.write("#{chained}\t#{date}")
end
ArchiveWorker.perform_async(id)
end
def price_without_taxe(invoice)

View File

@ -44,6 +44,7 @@ class NotificationType
notify_member_reservation_reminder
notify_admin_free_disk_space
notify_admin_close_period_reminder
notify_admin_archive_complete
]
# deprecated:
# - notify_member_subscribed_plan_is_changed

View File

@ -0,0 +1,7 @@
json.title notification.notification_type
json.description t('.archive_complete',
START: notification.attached_object.start_at,
END: notification.attached_object.end_at,
ID: notification.attached_object.id
)
json.url notification_url(notification, format: :json)

View File

@ -0,0 +1,12 @@
<%= render 'notifications_mailer/shared/hello', recipient: @recipient %>
<p>
<%= t('.body.archive_complete', START: @attached_object.start_at, END: @attached_object.end_at) %>
</p>
<p>
<%= t('.body.click_to_download') %>
<%=link_to( t('.body.here'), "#{root_url}api/accounting_periods/#{@attached_object.id}/archive", target: "_blank" )%>
</p>
<p>
<%= t('.body.save_on_secured') %>
</p>

View File

@ -0,0 +1,54 @@
# frozen_string_literal: true
# Will generate a ZIP archive file containing all invoicing data for the given period.
# This file will be asynchronously generated by sidekiq and a notification will be sent to the requesting user when it's done.
class ArchiveWorker
include Sidekiq::Worker
def perform(accounting_period_id)
period = AccountingPeriod.find(accounting_period_id)
data = period.invoices.includes(:invoice_items).order(id: :asc)
previous_file = period.previous_period&.archive_file
last_archive_checksum = previous_file ? Checksum.file(previous_file) : nil
json_data = to_json_archive(period, data, previous_file, last_archive_checksum)
current_archive_checksum = Checksum.text(json_data)
date = DateTime.iso8601
chained = Checksum.text("#{current_archive_checksum}#{last_archive_checksum}#{date}")
Zip::OutputStream.open(period.archive_file) do |io|
io.put_next_entry(period.archive_json_file)
io.write(json_data)
io.put_next_entry('checksum.sha256')
io.write("#{current_archive_checksum}\t#{period.archive_json_file}")
io.put_next_entry('chained.sha256')
io.write("#{chained}\t#{date}")
end
NotificationCenter.call type: :notify_admin_archive_complete,
receiver: User.find(period.closed_by),
attached_object: period
end
private
def to_json_archive(period, invoices, previous_file, last_checksum)
code_checksum = Checksum.code
ApplicationController.new.view_context.render(
partial: 'archive/accounting',
locals: {
invoices: period.invoices_with_vat(invoices),
period_total: period.period_total,
perpetual_total: period.perpetual_total,
period_footprint: period.footprint,
code_checksum: code_checksum,
last_archive_checksum: last_checksum,
previous_file: previous_file,
software_version: Version.current,
date: Time.now.iso8601
},
formats: [:json],
handlers: [:jbuilder]
)
end
end

View File

@ -424,7 +424,7 @@ en:
confirm_close_START_END: "Do you really want to close the accounting period between {{START}} and {{END}}? Any subsequent changes will be impossible."
period_must_match_fiscal_year: "A closing must occur at the end of a minimum annual period, or per financial year when it is not calendar-based."
this_may_take_a_while: "This operation will take some time to complete."
period_START_END_closed_success: "The accounting period from {{START}} to {{END}} has been successfully closed"
period_START_END_closed_success: "The accounting period from {{START}} to {{END}} has been successfully closed. Archive generation is running, you'll be notified when it's done."
failed_to_close_period: "An error occurred, unable to close the accounting period"
no_periods: "No closings for now"

View File

@ -424,7 +424,7 @@ es:
confirm_close_START_END: "Do you really want to close the accounting period between {{START}} and {{END}}? Any subsequent changes will be impossible." # translation_missing
period_must_match_fiscal_year: "A closing must occur at the end of a minimum annual period, or per financial year when it is not calendar-based." # translation_missing
this_may_take_a_while: "This operation will take some time to complete." # translation_missing
period_START_END_closed_success: "The accounting period from {{START}} to {{END}} has been successfully closed" # translation_missing
period_START_END_closed_success: "The accounting period from {{START}} to {{END}} has been successfully closed. Archive generation is running, you'll be notified when it's done." # translation_missing
failed_to_close_period: "An error occurred, unable to close the accounting period" # translation_missing
no_periods: "No closings for now" # translation_missing

View File

@ -424,7 +424,7 @@ fr:
confirm_close_START_END: "Êtes-vous sur de vouloir clôturer la période comptable du {{START}} au {{END}} ? Toute modification ultérieure sera impossible."
period_must_match_fiscal_year: "Une clôture doit intervenir à l'issue d'une période au minimum annuelle, ou par exercice lorsque celui-ci n'est pas calé sur l'année civile."
this_may_take_a_while: "Cette opération va prendre un certain temps."
period_START_END_closed_success: "La période comptable du {{START}} au {{END}} a bien été clôturée"
period_START_END_closed_success: "La période comptable du {{START}} au {{END}} a bien été clôturée. La génération de l'archive est en cours, vous serez prévenu lorsque celle-ci sera terminée."
failed_to_close_period: "Une erreur est survenue, impossible de clôturer la période comptable"
no_periods: "Aucune clôture pour le moment"

View File

@ -424,7 +424,7 @@ pt:
confirm_close_START_END: "Do you really want to close the accounting period between {{START}} and {{END}}? Any subsequent changes will be impossible" # translation_missing
period_must_match_fiscal_year: "A closing must occur at the end of a minimum annual period, or per financial year when it is not calendar-based." # translation_missing
this_may_take_a_while: "This operation will take some time to complete." # translation_missing
period_START_END_closed_success: "The accounting period from {{START}} to {{END}} has been successfully closed" # translation_missing
period_START_END_closed_success: "The accounting period from {{START}} to {{END}} has been successfully closed. Archive generation is running, you'll be notified when it's done." # translation_missing
failed_to_close_period: "An error occurred, unable to close the accounting period" # translation_missing
no_periods: "No closings for now" # translation_missing

View File

@ -316,6 +316,8 @@ en:
notify_admin_close_period_reminder:
warning_last_closed_period_over_1_year: "Please remind to periodically close your accounting periods. Last closed period ended at %{LAST_END}"
warning_no_closed_periods: "Please remind to periodically close your accounting periods. You have to close periods from %{FIRST_DATE}"
notify_admin_archive_complete:
archive_complete: "Data archiving from %{START} to %{END} is done. <a href='api/accounting_periods/%{ID}/archive' target='_blank'>click here to download</a>. Remember to save it on an external secured media."
statistics:
# statistics tools for admins

View File

@ -316,6 +316,8 @@ es:
notify_admin_close_period_reminder:
warning_last_closed_period_over_1_year: "Please remind to periodically close your accounting periods. Last closed period ended at %{LAST_END}" # missing translation
warning_no_closed_periods: "Please remind to periodically close your accounting periods. You have to close periods from %{FIRST_DATE}" # missing translation
notify_admin_archive_complete: # missing translation
archive_complete: "Data archiving from %{START} to %{END} is done. <a href='api/accounting_periods/%{ID}/archive' target='_blank'>click here to download</a>. Remember to save it on an external secured media." # missing translation
statistics:
# statistics tools for admins

View File

@ -316,6 +316,8 @@ fr:
notify_admin_close_period_reminder:
warning_last_closed_period_over_1_year: "Pensez à clôturer régulièrement vos périodes comptables. Les comptes sont actuellement clôturés jusqu'au %{LAST_END}"
warning_no_closed_periods: "Pensez à clôturer régulièrement vos périodes comptables. Vous devez clôturer des périodes depuis le %{FIRST_DATE}"
notify_admin_archive_complete:
archive_complete: "L'archivage des données du %{START} au %{END} est terminé. <a href='api/accounting_periods/%{ID}/archive' target='_blank'>Cliquez ici pour la télécharger</a>. Pensez à l'enregistrer sur un support externe sécurisé."
statistics:
# outil de statistiques pour les administrateurs

View File

@ -286,5 +286,13 @@ en:
warning_last_closed_period_over_1_year: "Please remind to periodically close your accounting periods. Last closed period ended at %{LAST_END}."
warning_no_closed_periods: "Please remind to periodically close your accounting periods. You have to close periods from %{FIRST_DATE}."
notify_admin_archive_complete:
subject: "Archiving completed"
body:
archive_complete: "You have closed the accounting period from %{START} to %{END}. Archiving of data is now complete."
click_to_download: "To download the ZIP archive, click"
here: "here."
save_on_secured: "Remember that you must save this archive on a secured external support, which may be requested by the tax authorities during a check."
shared:
hello: "Hello %{user_name}"

View File

@ -285,5 +285,13 @@ es:
warning_last_closed_period_over_1_year: "Please remind to periodically close your accounting periods. Last closed period ended at %{LAST_END}."
warning_no_closed_periods: "Please remind to periodically close your accounting periods. You have to close periods from %{FIRST_DATE}."
notify_admin_archive_complete: #translation_missing
subject: "Archiving completed"
body:
archive_complete: "You have closed the accounting period from %{START} to %{END}. Archiving of data is now complete."
click_to_download: "To download the ZIP archive, click"
here: "here."
save_on_secured: "Remember that you must save this archive on a secured external support, which may be requested by the tax authorities during a check."
shared:
hello: "¡Hola %{user_name}!"

View File

@ -286,5 +286,13 @@ fr:
warning_last_closed_period_over_1_year: "Pensez à clôturer régulièrement vos périodes comptables. Les comptes sont actuellement clôturés jusqu'au %{LAST_END}."
warning_no_closed_periods: "Pensez à clôturer régulièrement vos périodes comptables. Vous devez clôturer des périodes depuis le %{FIRST_DATE}."
notify_admin_archive_complete:
subject: "Archivage terminé"
body:
archive_complete: "Vous avez clôturé la période comptable du %{START} au %{END}. L'archivage des données est maintenant terminé."
click_to_download: "Pour télécharger l'archive ZIP, cliquez"
here: "ici."
save_on_secured: "N'oubliez pas que vous devez obligatoirement enregistrer cette archive sur un support externe sécurisé, qui peut vous être demandé par l'administration fiscale lors d'un contrôle."
shared:
hello: "Bonjour %{user_name}"

View File

@ -286,5 +286,13 @@ pt:
warning_last_closed_period_over_1_year: "Please remind to periodically close your accounting periods. Last closed period ended at %{LAST_END}."
warning_no_closed_periods: "Please remind to periodically close your accounting periods. You have to close periods from %{FIRST_DATE}."
notify_admin_archive_complete: #translation_missing
subject: "Archiving completed"
body:
archive_complete: "You have closed the accounting period from %{START} to %{END}. Archiving of data is now complete."
click_to_download: "To download the ZIP archive, click"
here: "here."
save_on_secured: "Remember that you must save this archive on a secured external support, which may be requested by the tax authorities during a check."
shared:
hello: "Olá %{user_name}"

View File

@ -316,6 +316,8 @@ pt:
notify_admin_close_period_reminder:
warning_last_closed_period_over_1_year: "Please remind to periodically close your accounting periods. Last closed period ended at %{LAST_END}" # missing translation
warning_no_closed_periods: "Please remind to periodically close your accounting periods. You have to close periods from %{FIRST_DATE}" # missing translation
notify_admin_archive_complete: # missing translation
archive_complete: "Data archiving from %{START} to %{END} is done. <a href='api/accounting_periods/%{ID}/archive' target='_blank'>click here to download</a>. Remember to save it on an external secured media." # missing translation
statistics:
# statistics tools for admins