diff --git a/app/services/invoices/number_service.rb b/app/services/invoices/number_service.rb index 63875cb1a..f247fe1e4 100644 --- a/app/services/invoices/number_service.rb +++ b/app/services/invoices/number_service.rb @@ -18,12 +18,24 @@ class Invoices::NumberService saved_number = setting == 'invoice_reference' ? document.reference : document.order_number return nil if saved_number.nil? - pattern = pattern(document, setting) - pattern = PaymentDocumentService.send(:replace_document_type_pattern, document, pattern) - start_idx = pattern.index(/n+|y+|m+|d+/) - end_idx = pattern.rindex(/n+|y+|m+|d+/) + indices = number_indices(document, setting) + saved_number[indices[0]..indices[1]]&.to_i + end - saved_number[start_idx..end_idx]&.to_i + # Replace the number of the reference of the given document and return the new reference + # @param document [PaymentDocument,NilClass] + # @param setting [String] 'invoice_reference' | 'invoice_order-nb' + # @return [String,NilClass] + def change_number(document, new_number, setting = 'invoice_reference') + raise TypeError, "invalid setting #{setting}" unless %w[invoice_order-nb invoice_reference].include?(setting) + return nil if document.nil? + + saved_number = setting == 'invoice_reference' ? document.reference : document.order_number + return nil if saved_number.nil? + + indices = number_indices(document, setting) + saved_number[indices[0]..indices[1]] = pad_and_truncate(new_number, indices[1] - indices[0]) + saved_number end # @param document [PaymentDocument,NilClass] @@ -58,5 +70,30 @@ class Invoices::NumberService Setting.get(setting) end end + + private + + # Output the given integer with leading zeros. If the given value is longer than the given + # length, it will be truncated. + # @param value [Integer] the integer to pad + # @param length [Integer] the length of the resulting string. + def pad_and_truncate(value, length) + value.to_s.rjust(length, '0').gsub(/^.*(.{#{length},}?)$/m, '\1') + end + + # Return the indices of the number in the document's reference + # @param document [PaymentDocument,NilClass] + # @param setting [String] 'invoice_reference' | 'invoice_order-nb' + # @return [Array] + def number_indices(document, setting = 'invoice_reference') + raise TypeError, "invalid setting #{setting}" unless %w[invoice_order-nb invoice_reference].include?(setting) + return nil if document.nil? + + pattern = pattern(document, setting) + pattern = PaymentDocumentService.send(:replace_document_type_pattern, document, pattern) + start_idx = pattern.index(/n+|y+|m+|d+/) + end_idx = pattern.rindex(/n+|y+|m+|d+/) + [start_idx, end_idx] + end end end diff --git a/lib/tasks/fablab/fix_references.rake b/lib/tasks/fablab/fix_references.rake new file mode 100644 index 000000000..77240ffa9 --- /dev/null +++ b/lib/tasks/fablab/fix_references.rake @@ -0,0 +1,69 @@ +# frozen_string_literal: true + +require 'integrity/archive_helper' + +namespace :fablab do + desc 'Fill the holes in the logical sequence of invoices references and regenerate invoices w/ duplicate reference' + task fix_references: :environment do |_task, _args| + include ActionView::Helpers::NumberHelper + + user = User.adminsys || User.admins.first + + # check the footprints + Integrity::ArchiveHelper.check_footprints + ActiveRecord::Base.transaction do + missing_references = {} + + # browse invoices to list missing reference + not_closed(Invoice).order(created_at: :desc).each do |invoice| + number = Invoices::NumberService.number(invoice) + next if number == 1 + + previous = Invoices::NumberService.change_number(invoice, number - 1) + next unless Invoice.find_by(reference: previous).nil? + + missing_references[invoice.created_at] ||= [] + missing_references[invoice.created_at].push(previous) + end + + # create placeholder invoices for found missing references + missing_references.each_pair do |date, references| + references.reverse_each.with_index do |reference, index| + Invoice.create!( + total: 0, + invoicing_profile: user.invoicing_profile, + statistic_profile: user.statistic_profile, + operator_profile: user.invoicing_profile, + payment_method: '', + reference: reference, + created_at: date - (index + 1).seconds, + description: 'Facture à néant, saut de facturation suite à un dysfonctionnement du logiciel Fab Manager', + invoice_items_attributes: [{ + amount: 0, + description: 'facture à zéro', + object_type: 'Error', + object_id: 1, + main: true + }] + ) + end + end + + # chain records + puts 'Chaining all record. This may take a while...' + not_closed(InvoiceItem).order(:id).find_each(&:chain_record) + not_closed(Invoice).order(:id).find_each(&:chain_record) + end + end + + # @param klass [Class] + # @return [ActiveRecord::Relation,Class] + def not_closed(klass) + if AccountingPeriod.count.positive? + last_period = AccountingPeriod.order(start_at: :desc).first + klass.where('created_at > ?', last_period.end_at) + else + klass + end + end +end diff --git a/test/helpers/invoice_helper.rb b/test/helpers/invoice_helper.rb index d63ed0f9b..37fb32ae1 100644 --- a/test/helpers/invoice_helper.rb +++ b/test/helpers/invoice_helper.rb @@ -42,6 +42,7 @@ module InvoiceHelper ) reservation.save invoice = Invoice.new( + total: 1000, invoicing_profile: customer.invoicing_profile, statistic_profile: customer.statistic_profile, operator_profile: operator.invoicing_profile,